diff --git a/c/driver_manager/adbc_driver_manager.cc b/c/driver_manager/adbc_driver_manager.cc index 9397b142f5..d15252b5dc 100644 --- a/c/driver_manager/adbc_driver_manager.cc +++ b/c/driver_manager/adbc_driver_manager.cc @@ -46,6 +46,7 @@ #include #include #include +#include #include #include #include @@ -65,6 +66,7 @@ std::filesystem::path InternalAdbcSystemConfigDir(); struct ParseDriverUriResult { std::string_view driver; std::optional uri; + std::optional profile; }; ADBC_EXPORT @@ -87,16 +89,32 @@ enum class SearchPathSource { kOtherError, }; +enum class SearchPathType { + kManifest, + kProfile, +}; + using SearchPaths = std::vector>; -void AddSearchPathsToError(const SearchPaths& search_paths, std::string& error_message) { +void AddSearchPathsToError(const SearchPaths& search_paths, const SearchPathType& type, + std::string& error_message) { if (!search_paths.empty()) { - error_message += "\nAlso searched these paths for manifests:"; + error_message += "\nAlso searched these paths for"; + if (type == SearchPathType::kManifest) { + error_message += " manifests:"; + } else if (type == SearchPathType::kProfile) { + error_message += " profiles:"; + } + for (const auto& [source, path] : search_paths) { error_message += "\n\t"; switch (source) { case SearchPathSource::kEnv: - error_message += "ADBC_DRIVER_PATH: "; + if (type == SearchPathType::kManifest) { + error_message += "ADBC_DRIVER_PATH: "; + } else if (type == SearchPathType::kProfile) { + error_message += "ADBC_PROFILE_PATH: "; + } break; case SearchPathSource::kUser: error_message += "user config dir: "; @@ -234,7 +252,7 @@ void SetError(struct AdbcError* error, struct AdbcError* src_error) { if (src_error->message) { size_t message_size = strlen(src_error->message); - error->message = new char[message_size]; + error->message = new char[message_size + 1]; // +1 to include null std::memcpy(error->message, src_error->message, message_size); error->message[message_size] = '\0'; } else { @@ -398,6 +416,59 @@ AdbcStatusCode LoadDriverFromRegistry(HKEY root, const std::wstring& driver_name } #endif // _WIN32 +#define CHECK_STATUS(EXPR) \ + if (auto _status = (EXPR); _status != ADBC_STATUS_OK) { \ + return _status; \ + } + +AdbcStatusCode ProcessProfileValue(std::string_view value, std::string& out, + struct AdbcError* error) { + if (value.empty()) { + SetError(error, "Profile value is null"); + return ADBC_STATUS_INVALID_ARGUMENT; + } + + const auto pos = value.find("env_var("); + if (pos == std::string_view::npos || pos != 0) { + out = std::string(value); + return ADBC_STATUS_OK; + } + + if (value[value.size() - 1] != ')') { + SetError(error, "Malformed env_var() profile value: missing closing parenthesis"); + return ADBC_STATUS_INVALID_ARGUMENT; + } + + // Extract the environment variable name from the value + // which should be formatted as env_var(VAR_NAME) as we confirmed + // above. + const auto env_var_name = value.substr(8, value.size() - 9); +#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); + out = Utf8Encode(wvalue); +#else + const char* env_value = std::getenv(std::string(env_var_name).c_str()); + if (!env_value) { + out = ""; + return ADBC_STATUS_OK; + } + out = std::string(env_value); +#endif + return ADBC_STATUS_OK; +} + /// \return ADBC_STATUS_NOT_FOUND if the manifest does not contain a driver /// path for this platform, ADBC_STATUS_INVALID_ARGUMENT if the manifest /// could not be parsed, ADBC_STATUS_OK otherwise (`info` will be populated) @@ -520,8 +591,10 @@ SearchPaths GetEnvPaths(const char_type* env_var) { #ifdef _WIN32 static const wchar_t* kAdbcDriverPath = L"ADBC_DRIVER_PATH"; +static const wchar_t* kAdbcProfilePath = L"ADBC_PROFILE_PATH"; #else static const char* kAdbcDriverPath = "ADBC_DRIVER_PATH"; +static const char* kAdbcProfilePath = "ADBC_PROFILE_PATH"; #endif // _WIN32 SearchPaths GetSearchPaths(const AdbcLoadFlags levels) { @@ -728,7 +801,7 @@ struct ManagedLibrary { extra_debug_info.end()); if (intermediate_error.error.message) { std::string error_message = intermediate_error.error.message; - AddSearchPathsToError(search_paths, error_message); + AddSearchPathsToError(search_paths, SearchPathType::kManifest, error_message); SetError(error, std::move(error_message)); } return status; @@ -947,7 +1020,7 @@ struct ManagedLibrary { error_message += "\n"; error_message += message; } - AddSearchPathsToError(attempted_paths, error_message); + AddSearchPathsToError(attempted_paths, SearchPathType::kManifest, error_message); SetError(error, error_message); return ADBC_STATUS_NOT_FOUND; } else { @@ -1000,7 +1073,7 @@ struct ManagedLibrary { error_message += "\n"; error_message += message; } - AddSearchPathsToError(attempted_paths, error_message); + AddSearchPathsToError(attempted_paths, SearchPathType::kManifest, error_message); SetError(error, error_message); return ADBC_STATUS_NOT_FOUND; } @@ -1042,6 +1115,262 @@ struct ManagedLibrary { #endif // defined(_WIN32) }; +struct FilesystemProfile { + std::filesystem::path path; + std::string driver; + std::unordered_map options; + std::unordered_map int_options; + std::unordered_map double_options; + + std::vector options_keys; + std::vector options_values; + + std::vector int_option_keys; + std::vector int_option_values; + + std::vector double_option_keys; + std::vector double_option_values; + + static void populate_connection_profile(FilesystemProfile&& profile, + struct AdbcConnectionProfile* out) { + profile.options_keys.reserve(profile.options.size()); + profile.options_values.reserve(profile.options.size()); + for (const auto& [key, value] : profile.options) { + profile.options_keys.push_back(key.c_str()); + profile.options_values.push_back(value.c_str()); + } + + profile.int_option_keys.reserve(profile.int_options.size()); + profile.int_option_values.reserve(profile.int_options.size()); + for (const auto& [key, value] : profile.int_options) { + profile.int_option_keys.push_back(key.c_str()); + profile.int_option_values.push_back(value); + } + + profile.double_option_keys.reserve(profile.double_options.size()); + profile.double_option_values.reserve(profile.double_options.size()); + for (const auto& [key, value] : profile.double_options) { + profile.double_option_keys.push_back(key.c_str()); + profile.double_option_values.push_back(value); + } + + out->private_data = new FilesystemProfile(std::move(profile)); + out->release = [](AdbcConnectionProfile* profile) { + if (!profile || !profile->private_data) { + return; + } + + delete static_cast(profile->private_data); + profile->private_data = nullptr; + profile->release = nullptr; + }; + + out->GetDriverName = [](AdbcConnectionProfile* profile, const char** out, + struct AdbcError* error) -> AdbcStatusCode { + if (!profile || !profile->private_data) { + SetError(error, "Invalid connection profile"); + return ADBC_STATUS_INVALID_ARGUMENT; + } + + auto* fs_profile = static_cast(profile->private_data); + *out = fs_profile->driver.c_str(); + return ADBC_STATUS_OK; + }; + + out->GetOptions = [](AdbcConnectionProfile* profile, const char*** keys, + const char*** values, size_t* num_options, + struct AdbcError* error) -> AdbcStatusCode { + if (!profile || !profile->private_data) { + SetError(error, "Invalid connection profile"); + return ADBC_STATUS_INVALID_ARGUMENT; + } + + if (!keys || !values || !num_options) { + SetError(error, "Output parameters cannot be null"); + return ADBC_STATUS_INVALID_ARGUMENT; + } + + auto* fs_profile = static_cast(profile->private_data); + *num_options = fs_profile->options.size(); + *keys = fs_profile->options_keys.data(); + *values = fs_profile->options_values.data(); + return ADBC_STATUS_OK; + }; + + out->GetIntOptions = [](AdbcConnectionProfile* profile, const char*** keys, + const int64_t** values, size_t* num_options, + struct AdbcError* error) -> AdbcStatusCode { + if (!profile || !profile->private_data) { + SetError(error, "Invalid connection profile"); + return ADBC_STATUS_INVALID_ARGUMENT; + } + + if (!keys || !values || !num_options) { + SetError(error, "Output parameters cannot be null"); + return ADBC_STATUS_INVALID_ARGUMENT; + } + + auto* fs_profile = static_cast(profile->private_data); + *num_options = fs_profile->int_options.size(); + *keys = fs_profile->int_option_keys.data(); + *values = fs_profile->int_option_values.data(); + return ADBC_STATUS_OK; + }; + + out->GetDoubleOptions = [](AdbcConnectionProfile* profile, const char*** keys, + const double** values, size_t* num_options, + struct AdbcError* error) -> AdbcStatusCode { + if (!profile || !profile->private_data) { + SetError(error, "Invalid connection profile"); + return ADBC_STATUS_INVALID_ARGUMENT; + } + + if (!keys || !values || !num_options) { + SetError(error, "Output parameters cannot be null"); + return ADBC_STATUS_INVALID_ARGUMENT; + } + + auto* fs_profile = static_cast(profile->private_data); + *num_options = fs_profile->double_options.size(); + *keys = fs_profile->double_option_keys.data(); + *values = fs_profile->double_option_values.data(); + return ADBC_STATUS_OK; + }; + } +}; + +struct profileVisitor { + FilesystemProfile& profile; + const std::filesystem::path& profile_path; + struct AdbcError* error; + + bool visit_table(const std::string& prefix, toml::table& table) { + for (const auto& [key, value] : table) { + if (auto* str = value.as_string()) { + profile.options[prefix + key.data()] = str->get(); + } else if (auto* int_val = value.as_integer()) { + profile.int_options[prefix + key.data()] = int_val->get(); + } else if (auto* double_val = value.as_floating_point()) { + profile.double_options[prefix + key.data()] = double_val->get(); + } else if (auto* bool_val = value.as_boolean()) { + profile.options[prefix + key.data()] = bool_val->get() ? "true" : "false"; + } else if (value.is_table()) { + if (!visit_table(prefix + key.data() + ".", *value.as_table())) { + return false; + } + } else { + std::string message = "Unsupported value type for key '" + + std::string(key.str()) + "' in profile '" + + profile_path.string() + "'"; + SetError(error, std::move(message)); + return false; + } + } + return !error->message; + } +}; + +AdbcStatusCode LoadProfileFile(const std::filesystem::path& profile_path, + FilesystemProfile& profile, struct AdbcError* error) { + toml::table config; + try { + config = toml::parse_file(profile_path.native()); + } catch (const toml::parse_error& err) { + std::string message = "Could not open profile. "; + message += err.what(); + message += ". Profile: "; + message += profile_path.string(); + SetError(error, std::move(message)); + return ADBC_STATUS_INVALID_ARGUMENT; + } + + profile.path = profile_path; + if (!config["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)); + switch (version) { + case 1: + break; + default: { + std::string message = + "Profile version '" + std::to_string(version) + + "' is not supported by this driver manager. Profile: " + profile_path.string(); + SetError(error, std::move(message)); + return ADBC_STATUS_INVALID_ARGUMENT; + } + } + + profile.driver = config["driver"].value_or(""s); + + auto options = config.at_path("options"); + if (!options.is_table()) { + std::string message = + "Profile options is not a table in profile '" + profile_path.string() + "'"; + SetError(error, std::move(message)); + return ADBC_STATUS_INVALID_ARGUMENT; + } + + auto* options_table = options.as_table(); + profileVisitor v{profile, profile_path, error}; + if (!v.visit_table("", *options_table)) { + return ADBC_STATUS_INVALID_ARGUMENT; + } + + return ADBC_STATUS_OK; +} + +SearchPaths GetProfileSearchPaths(const char* additional_search_path_list) { + SearchPaths search_paths; + { + std::vector additional_paths; + if (additional_search_path_list) { + additional_paths = InternalAdbcParsePath(additional_search_path_list); + } + + for (const auto& path : additional_paths) { + search_paths.emplace_back(SearchPathSource::kAdditional, path); + } + } + + { + auto env_paths = GetEnvPaths(kAdbcProfilePath); + search_paths.insert(search_paths.end(), env_paths.begin(), env_paths.end()); + } + +#if ADBC_CONDA_BUILD +#ifdef _WIN32 + const wchar_t* conda_name = L"CONDA_PREFIX"; +#else + const char* conda_name = "CONDA_PREFIX"; +#endif // _WIN32 + + auto venv = GetEnvPaths(conda_name); + for (const auto& [_, venv_path] : venv) { + search_paths.emplace_back(SearchPathSource::kConda, + venv_path / "etc" / "adbc" / "profiles"); + } +#else + search_paths.emplace_back(SearchPathSource::kDisabledAtCompileTime, "Conda prefix"); +#endif // ADBC_CONDA_BUILD + +#ifdef _WIN32 + const wchar_t* profiles_dir = L"Profiles"; +#elif defined(__APPLE__) + const char* profiles_dir = "Profiles"; +#else + const char* profiles_dir = "profiles"; +#endif // defined(_WIN32) + + auto user_dir = InternalAdbcUserConfigDir().parent_path() / profiles_dir; + search_paths.emplace_back(SearchPathSource::kUser, user_dir); + return search_paths; +} + /// Hold the driver DLL and the driver release callback in the driver struct. struct ManagerDriverState { // The original release callback @@ -1417,6 +1746,7 @@ struct TempDatabase { AdbcDriverInitFunc init_func = nullptr; AdbcLoadFlags load_flags = ADBC_LOAD_FLAG_ALLOW_RELATIVE_PATHS; std::string additional_search_path_list; + AdbcConnectionProfileProvider profile_provider = nullptr; }; /// Temporary state while the database is being configured. @@ -1425,14 +1755,14 @@ struct TempConnection { std::unordered_map bytes_options; std::unordered_map int_options; std::unordered_map double_options; + AdbcConnectionProfile* connection_profile = nullptr; }; static const char kDefaultEntrypoint[] = "AdbcDriverInit"; } // namespace // Other helpers (intentionally not in an anonymous namespace so they can be tested) -ADBC_EXPORT -std::filesystem::path InternalAdbcUserConfigDir() { +ADBC_EXPORT std::filesystem::path InternalAdbcUserConfigDir() { std::filesystem::path config_dir; #if defined(_WIN32) // SHGetFolderPath is just an alias to SHGetKnownFolderPath since Vista @@ -1600,22 +1930,26 @@ std::optional InternalAdbcParseDriverUri(std::string_view std::string_view d = str.substr(0, pos); if (str.size() <= pos + 1) { - return ParseDriverUriResult{d, std::nullopt}; + return ParseDriverUriResult{d, std::nullopt, std::nullopt}; } #ifdef _WIN32 if (std::filesystem::exists(std::filesystem::path(str))) { // No scheme, just a path - return ParseDriverUriResult{str, std::nullopt}; + return ParseDriverUriResult{str, std::nullopt, std::nullopt}; } #endif if (str[pos + 1] == '/') { // scheme is also driver - return ParseDriverUriResult{d, str}; + if (d == "profile" && str.size() > pos + 2) { + // found a profile URI "profile://" + return ParseDriverUriResult{"", std::nullopt, str.substr(pos + 3)}; + } + return ParseDriverUriResult{d, str, std::nullopt}; } // driver:scheme:..... - return ParseDriverUriResult{d, str.substr(pos + 1)}; + return ParseDriverUriResult{d, str.substr(pos + 1), std::nullopt}; } // Direct implementations of API methods @@ -1672,6 +2006,19 @@ AdbcStatusCode AdbcDatabaseNew(struct AdbcDatabase* database, struct AdbcError* return ADBC_STATUS_OK; } +AdbcStatusCode AdbcDriverManagerDatabaseSetProfileProvider( + struct AdbcDatabase* database, AdbcConnectionProfileProvider provider, + struct AdbcError* error) { + if (database->private_driver) { + SetError(error, "Cannot SetProfileProvider after AdbcDatabaseInit"); + return ADBC_STATUS_INVALID_STATE; + } + + TempDatabase* args = reinterpret_cast(database->private_data); + args->profile_provider = provider; + return ADBC_STATUS_OK; +} + AdbcStatusCode AdbcDatabaseGetOption(struct AdbcDatabase* database, const char* key, char* value, size_t* length, struct AdbcError* error) { @@ -1857,6 +2204,162 @@ AdbcStatusCode AdbcDriverManagerDatabaseSetInitFunc(struct AdbcDatabase* databas return ADBC_STATUS_OK; } +AdbcStatusCode AdbcFilesystemProfileProvider(const char* profile_name, + const char* additional_search_path_list, + struct AdbcConnectionProfile* out, + struct AdbcError* error) { + if (profile_name == nullptr || strlen(profile_name) == 0) { + SetError(error, "Profile name is empty"); + return ADBC_STATUS_INVALID_ARGUMENT; + } + + if (!out) { + SetError(error, "Output profile is null"); + return ADBC_STATUS_INVALID_ARGUMENT; + } + + std::memset(out, 0, sizeof(*out)); + std::filesystem::path profile_path(profile_name); + if (profile_path.has_extension()) { + if (HasExtension(profile_path, ".toml")) { + if (!std::filesystem::exists(profile_path)) { + SetError(error, "Profile file does not exist: " + profile_path.string()); + return ADBC_STATUS_NOT_FOUND; + } + + FilesystemProfile profile; + CHECK_STATUS(LoadProfileFile(profile_path, profile, error)); + FilesystemProfile::populate_connection_profile(std::move(profile), out); + return ADBC_STATUS_OK; + } + } + + if (profile_path.is_absolute()) { + profile_path.replace_extension(".toml"); + + FilesystemProfile profile; + CHECK_STATUS(LoadProfileFile(profile_path, profile, error)); + FilesystemProfile::populate_connection_profile(std::move(profile), out); + return ADBC_STATUS_OK; + } + + SearchPaths search_paths = GetProfileSearchPaths(additional_search_path_list); + SearchPaths extra_debug_info; + for (const auto& [source, search_path] : search_paths) { + if (source == SearchPathSource::kRegistry || source == SearchPathSource::kUnset || + source == SearchPathSource::kDoesNotExist || + source == SearchPathSource::kDisabledAtCompileTime || + source == SearchPathSource::kDisabledAtRunTime || + source == SearchPathSource::kOtherError) { + continue; + } + + std::filesystem::path full_path = search_path / profile_path; + full_path.replace_extension(".toml"); + if (std::filesystem::exists(full_path)) { + OwnedError intermediate_error; + + FilesystemProfile profile; + auto status = LoadProfileFile(full_path, profile, &intermediate_error.error); + if (status == ADBC_STATUS_OK) { + FilesystemProfile::populate_connection_profile(std::move(profile), out); + return ADBC_STATUS_OK; + } else if (status == ADBC_STATUS_INVALID_ARGUMENT) { + search_paths.insert(search_paths.end(), extra_debug_info.begin(), + extra_debug_info.end()); + if (intermediate_error.error.message) { + std::string error_message = intermediate_error.error.message; + AddSearchPathsToError(search_paths, SearchPathType::kProfile, error_message); + SetError(error, std::move(error_message)); + } + return status; + } + + std::string message = "found "; + message += full_path.string(); + message += " but: "; + if (intermediate_error.error.message) { + message += intermediate_error.error.message; + } else { + message += "could not load the profile"; + } + extra_debug_info.emplace_back(SearchPathSource::kOtherError, std::move(message)); + } + } + + search_paths.insert(search_paths.end(), extra_debug_info.begin(), + extra_debug_info.end()); + std::string error_message = "Profile not found: " + std::string(profile_name); + AddSearchPathsToError(search_paths, SearchPathType::kProfile, error_message); + SetError(error, std::move(error_message)); + return ADBC_STATUS_NOT_FOUND; +} + +struct ProfileGuard { + AdbcConnectionProfile& profile; + explicit ProfileGuard(AdbcConnectionProfile& profile) : profile(profile) {} + ~ProfileGuard() { + if (profile.release) { + profile.release(&profile); + } + } +}; + +AdbcStatusCode InternalInitializeProfile(TempDatabase* args, + const std::string_view profile, + struct AdbcError* error) { + if (!args->profile_provider) { + args->profile_provider = AdbcFilesystemProfileProvider; + } + + AdbcConnectionProfile connection_profile{}; + CHECK_STATUS(args->profile_provider(profile.data(), + args->additional_search_path_list.c_str(), + &connection_profile, error)); + + ProfileGuard guard{connection_profile}; + const char* driver_name = nullptr; + CHECK_STATUS( + connection_profile.GetDriverName(&connection_profile, &driver_name, error)); + if (driver_name != nullptr && strlen(driver_name) > 0) { + args->driver = driver_name; + } + + const char** keys = nullptr; + const char** values = nullptr; + size_t num_options = 0; + const int64_t* int_values = nullptr; + const double* double_values = nullptr; + + CHECK_STATUS(connection_profile.GetOptions(&connection_profile, &keys, &values, + &num_options, error)); + for (size_t i = 0; i < num_options; ++i) { + // 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)); + args->options.try_emplace(keys[i], processed); + } + + CHECK_STATUS(connection_profile.GetIntOptions(&connection_profile, &keys, &int_values, + &num_options, error)); + for (size_t i = 0; i < num_options; ++i) { + // use try_emplace so we only add the option if there isn't + // already an option with the same name + args->int_options.try_emplace(keys[i], int_values[i]); + } + + CHECK_STATUS(connection_profile.GetDoubleOptions(&connection_profile, &keys, + &double_values, &num_options, error)); + for (size_t i = 0; i < num_options; ++i) { + // use try_emplace so we only add the option if there isn't already an option with the + // same name + args->double_options.try_emplace(keys[i], double_values[i]); + } + + return ADBC_STATUS_OK; +} + AdbcStatusCode AdbcDatabaseInit(struct AdbcDatabase* database, struct AdbcError* error) { if (!database->private_data) { SetError(error, "Must call AdbcDatabaseNew before AdbcDatabaseInit"); @@ -1864,13 +2367,25 @@ AdbcStatusCode AdbcDatabaseInit(struct AdbcDatabase* database, struct AdbcError* } TempDatabase* args = reinterpret_cast(database->private_data); if (!args->init_func) { + const auto profile_in_use = args->options.find("profile"); + if (profile_in_use != args->options.end()) { + std::string_view profile = profile_in_use->second; + CHECK_STATUS(InternalInitializeProfile(args, profile, error)); + args->options.erase("profile"); + } + const auto uri = args->options.find("uri"); if (args->driver.empty() && uri != args->options.end()) { std::string owned_uri = uri->second; auto result = InternalAdbcParseDriverUri(owned_uri); - if (result && result->uri) { - args->driver = std::string{result->driver}; - args->options["uri"] = std::string{*result->uri}; + if (result) { + if (result->uri) { + args->driver = std::string{result->driver}; + args->options["uri"] = std::string{*result->uri}; + } else if (result->profile) { + args->options.erase("uri"); + CHECK_STATUS(InternalInitializeProfile(args, *result->profile, error)); + } } } else if (!args->driver.empty() && uri == args->options.end()) { std::string owned_driver = args->driver; @@ -1879,6 +2394,8 @@ AdbcStatusCode AdbcDatabaseInit(struct AdbcDatabase* database, struct AdbcError* args->driver = std::string{result->driver}; if (result->uri) { args->options["uri"] = std::string{*result->uri}; + } else if (result->profile) { + CHECK_STATUS(InternalInitializeProfile(args, *result->profile, error)); } } } @@ -2217,6 +2734,7 @@ AdbcStatusCode AdbcConnectionInit(struct AdbcConnection* connection, SetError(error, "Database is not initialized"); return ADBC_STATUS_INVALID_ARGUMENT; } + TempConnection* args = reinterpret_cast(connection->private_data); connection->private_data = nullptr; std::unordered_map options = std::move(args->options); diff --git a/c/driver_manager/adbc_driver_manager_test.cc b/c/driver_manager/adbc_driver_manager_test.cc index 64400a01bb..c6bfd718a7 100644 --- a/c/driver_manager/adbc_driver_manager_test.cc +++ b/c/driver_manager/adbc_driver_manager_test.cc @@ -43,6 +43,7 @@ std::filesystem::path InternalAdbcUserConfigDir(); struct ParseDriverUriResult { std::string_view driver; std::optional uri; + std::optional profile; }; std::optional InternalAdbcParseDriverUri(std::string_view str); @@ -624,11 +625,15 @@ TEST(AdbcDriverManagerInternal, InternalAdbcParsePath) { TEST(AdbcDriverManagerInternal, InternalAdbcParseDriverUri) { std::vector>> uris = { {"sqlite", std::nullopt}, - {"sqlite:", {{"sqlite", std::nullopt}}}, - {"sqlite:file::memory:", {{"sqlite", "file::memory:"}}}, - {"sqlite:file::memory:?cache=shared", {{"sqlite", "file::memory:?cache=shared"}}}, + {"sqlite:", {{"sqlite", std::nullopt, std::nullopt}}}, + {"sqlite:file::memory:", {{"sqlite", "file::memory:", std::nullopt}}}, + {"sqlite:file::memory:?cache=shared", + {{"sqlite", "file::memory:?cache=shared", std::nullopt}}}, {"postgresql://a:b@localhost:9999/nonexistent", - {{"postgresql", "postgresql://a:b@localhost:9999/nonexistent"}}}}; + {{"postgresql", "postgresql://a:b@localhost:9999/nonexistent", std::nullopt}}}, + {"profile://foo_prof", {{"", std::nullopt, "foo_prof"}}}, + {"profile:///foo/bar/profile.toml", {{"", std::nullopt, "/foo/bar/profile.toml"}}}, + }; #ifdef _WIN32 auto temp_dir = std::filesystem::temp_directory_path() / "adbc_driver_manager_tests"; @@ -1460,4 +1465,361 @@ TEST_F(DriverManifest, ControlCodes) { } } +class ConnectionProfiles : public ::testing::Test { + public: + void SetUp() override { + std::memset(&driver, 0, sizeof(driver)); + std::memset(&error, 0, sizeof(error)); + + temp_dir = + std::filesystem::temp_directory_path() / "adbc_driver_manager_profile_test"; + std::filesystem::create_directories(temp_dir); + + simple_profile = toml::table{ + {"version", 1}, + {"driver", "adbc_driver_sqlite"}, + {"options", + toml::table{ + {"uri", "file::memory:"}, + }}, + }; + } + + void TearDown() override { + if (error.release) { + error.release(&error); + } + + if (driver.release) { + ASSERT_THAT(driver.release(&driver, &error), IsOkStatus(&error)); + ASSERT_EQ(driver.private_data, nullptr); + ASSERT_EQ(driver.private_manager, nullptr); + } + + driver_path.clear(); + if (std::filesystem::exists(temp_dir)) { + std::filesystem::remove_all(temp_dir); + } + } + + protected: + void SetConfigPath(const char* path) { +#ifdef _WIN32 + int size_needed = MultiByteToWideChar(CP_UTF8, 0, path, -1, nullptr, 0); + std::wstring wpath(size_needed, 0); + MultiByteToWideChar(CP_UTF8, 0, path, -1, &wpath[0], size_needed); + ASSERT_TRUE(SetEnvironmentVariableW(L"ADBC_PROFILE_PATH", wpath.c_str())); +#else + setenv("ADBC_PROFILE_PATH", path, 1); +#endif + } + + void UnsetConfigPath() { SetConfigPath(""); } + + struct AdbcDriver driver = {}; + struct AdbcError error = {}; + + std::filesystem::path temp_dir; + std::filesystem::path driver_path; + toml::table simple_profile; +}; + +TEST_F(ConnectionProfiles, SetProfileOption) { + auto filepath = temp_dir / "profile.toml"; + toml::table profile = simple_profile; + std::ofstream test_manifest_file(filepath); + ASSERT_TRUE(test_manifest_file.is_open()); + test_manifest_file << profile; + test_manifest_file.close(); + + adbc_validation::Handle database; + + // absolute path to the profile + ASSERT_THAT(AdbcDatabaseNew(&database.value, &error), IsOkStatus(&error)); + ASSERT_THAT(AdbcDatabaseSetOption(&database.value, "profile", filepath.string().c_str(), + &error), + IsOkStatus(&error)); + ASSERT_THAT(AdbcDatabaseInit(&database.value, &error), IsOkStatus(&error)); + ASSERT_THAT(AdbcDatabaseRelease(&database.value, &error), IsOkStatus(&error)); + + // inherit additional_search_path_list + ASSERT_THAT(AdbcDatabaseNew(&database.value, &error), IsOkStatus(&error)); + ASSERT_THAT(AdbcDatabaseSetOption(&database.value, "profile", "profile", &error), + IsOkStatus(&error)); + ASSERT_THAT(AdbcDriverManagerDatabaseSetAdditionalSearchPathList( + &database.value, temp_dir.string().c_str(), &error), + IsOkStatus(&error)); + ASSERT_THAT(AdbcDatabaseInit(&database.value, &error), IsOkStatus(&error)); + ASSERT_THAT(AdbcDatabaseRelease(&database.value, &error), IsOkStatus(&error)); + + // 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), IsOkStatus(&error)); + ASSERT_THAT(AdbcDatabaseRelease(&database.value, &error), IsOkStatus(&error)); + UnsetConfigPath(); +} + +TEST_F(ConnectionProfiles, HierarchicalProfile) { + auto filepath = temp_dir / "dev" / "profile.toml"; + std::filesystem::create_directories(filepath.parent_path()); + toml::table profile = simple_profile; + std::ofstream test_manifest_file(filepath); + ASSERT_TRUE(test_manifest_file.is_open()); + test_manifest_file << profile; + test_manifest_file.close(); + + adbc_validation::Handle database; + + SetConfigPath(temp_dir.string().c_str()); + ASSERT_THAT(AdbcDatabaseNew(&database.value, &error), IsOkStatus(&error)); + ASSERT_THAT(AdbcDatabaseSetOption(&database.value, "profile", "dev/profile", &error), + IsOkStatus(&error)); + ASSERT_THAT(AdbcDatabaseInit(&database.value, &error), IsOkStatus(&error)); + ASSERT_THAT(AdbcDatabaseRelease(&database.value, &error), IsOkStatus(&error)); + UnsetConfigPath(); +} + +TEST_F(ConnectionProfiles, UriProfileOption) { + auto filepath = temp_dir / "profile.toml"; + toml::table profile = simple_profile; + std::ofstream test_manifest_file(filepath); + ASSERT_TRUE(test_manifest_file.is_open()); + test_manifest_file << profile; + test_manifest_file.close(); + + adbc_validation::Handle database; + + // absolute path to the profile + ASSERT_THAT(AdbcDatabaseNew(&database.value, &error), IsOkStatus(&error)); + ASSERT_THAT(AdbcDatabaseSetOption(&database.value, "uri", + ("profile://" + filepath.string()).c_str(), &error), + IsOkStatus(&error)); + ASSERT_THAT(AdbcDatabaseInit(&database.value, &error), IsOkStatus(&error)); + ASSERT_THAT(AdbcDatabaseRelease(&database.value, &error), IsOkStatus(&error)); + + // 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, "uri", "profile://profile", &error), + IsOkStatus(&error)); + ASSERT_THAT(AdbcDatabaseInit(&database.value, &error), IsOkStatus(&error)); + ASSERT_THAT(AdbcDatabaseRelease(&database.value, &error), IsOkStatus(&error)); + UnsetConfigPath(); +} + +TEST_F(ConnectionProfiles, DriverProfileOption) { + auto filepath = temp_dir / "profile.toml"; + toml::table profile = simple_profile; + std::ofstream test_manifest_file(filepath); + ASSERT_TRUE(test_manifest_file.is_open()); + test_manifest_file << profile; + test_manifest_file.close(); + + adbc_validation::Handle database; + + // absolute path to the profile + ASSERT_THAT(AdbcDatabaseNew(&database.value, &error), IsOkStatus(&error)); + ASSERT_THAT(AdbcDatabaseSetOption(&database.value, "driver", + ("profile://" + filepath.string()).c_str(), &error), + IsOkStatus(&error)); + ASSERT_THAT(AdbcDatabaseInit(&database.value, &error), IsOkStatus(&error)); + ASSERT_THAT(AdbcDatabaseRelease(&database.value, &error), IsOkStatus(&error)); + + // 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, "driver", "profile://profile", &error), + IsOkStatus(&error)); + ASSERT_THAT(AdbcDatabaseInit(&database.value, &error), IsOkStatus(&error)); + ASSERT_THAT(AdbcDatabaseRelease(&database.value, &error), IsOkStatus(&error)); + UnsetConfigPath(); +} + +TEST_F(ConnectionProfiles, ExtraStringOption) { + auto filepath = temp_dir / "profile.toml"; + toml::table profile = simple_profile; + profile["options"].as_table()->insert("foo", "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 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='bar'")); + UnsetConfigPath(); +} + +TEST_F(ConnectionProfiles, ExtraIntOption) { + auto filepath = temp_dir / "profile.toml"; + toml::table profile = simple_profile; + 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; + test_manifest_file.close(); + + adbc_validation::Handle 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=42")); + UnsetConfigPath(); +} + +TEST_F(ConnectionProfiles, ExtraDoubleOption) { + auto filepath = temp_dir / "profile.toml"; + toml::table profile = simple_profile; + 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; + test_manifest_file.close(); + + adbc_validation::Handle 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=42")); + UnsetConfigPath(); +} + +TEST_F(ConnectionProfiles, DotSeparatedKey) { + auto filepath = temp_dir / "profile.toml"; + toml::table profile = toml::parse(R"( + version = 1 + driver = "adbc_driver_sqlite" + [options] + foo.bar.baz = "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 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.bar.baz='bar'")); + UnsetConfigPath(); +} + +TEST_F(ConnectionProfiles, UseEnvVar) { + auto filepath = temp_dir / "profile.toml"; + toml::table profile = toml::parse(R"|( + version = 1 + driver = "adbc_driver_sqlite" + [options] + foo = "env_var(ADBC_PROFILE_PATH)" + )|"); + + std::ofstream test_manifest_file(filepath); + ASSERT_TRUE(test_manifest_file.is_open()); + test_manifest_file << profile; + test_manifest_file.close(); + + adbc_validation::Handle 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='" + + temp_dir.string() + "'")); + UnsetConfigPath(); +} + +TEST_F(ConnectionProfiles, ProfileNotFound) { + adbc_validation::Handle database; + + // absolute path to the profile + ASSERT_THAT(AdbcDatabaseNew(&database.value, &error), IsOkStatus(&error)); + ASSERT_THAT(AdbcDatabaseSetOption(&database.value, "profile", + (temp_dir / "profile.toml").string().c_str(), &error), + IsOkStatus(&error)); + ASSERT_THAT(AdbcDatabaseInit(&database.value, &error), + IsStatus(ADBC_STATUS_NOT_FOUND, &error)); + ASSERT_THAT(error.message, ::testing::HasSubstr("Profile file does not exist: " + + (temp_dir / "profile.toml").string())); + ASSERT_THAT(AdbcDatabaseRelease(&database.value, &error), IsOkStatus(&error)); + + // 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_FOUND, &error)); + ASSERT_THAT(error.message, + ::testing::HasSubstr(std::string("Profile not found: profile\n") + + "Also searched these paths for profiles:\n\t" + + "ADBC_PROFILE_PATH: " + temp_dir.string() + "\n\t")); + ASSERT_THAT(AdbcDatabaseRelease(&database.value, &error), IsOkStatus(&error)); + UnsetConfigPath(); +} + +TEST_F(ConnectionProfiles, CustomProfileProvider) { + adbc_validation::Handle database; + + AdbcConnectionProfileProvider provider = + [](const char* profile_name, const char* additional_path_list, + struct AdbcConnectionProfile* out, struct AdbcError* error) -> AdbcStatusCode { + EXPECT_EQ(std::string(profile_name), "profile"); + + static const std::string expected = "custom profile provider error"; + error->message = new char[expected.size() + 1]; + std::copy(expected.begin(), expected.end(), error->message); + error->message[expected.size()] = '\0'; + error->release = [](struct AdbcError* error) { + delete[] error->message; + error->message = nullptr; + error->release = nullptr; + }; + return ADBC_STATUS_INVALID_ARGUMENT; + }; + + ASSERT_THAT(AdbcDatabaseNew(&database.value, &error), IsOkStatus(&error)); + ASSERT_THAT(AdbcDatabaseSetOption(&database.value, "profile", "profile", &error), + IsOkStatus(&error)); + ASSERT_THAT( + AdbcDriverManagerDatabaseSetProfileProvider(&database.value, provider, &error), + IsOkStatus(&error)); + ASSERT_THAT(AdbcDatabaseInit(&database.value, &error), + IsStatus(ADBC_STATUS_INVALID_ARGUMENT, &error)); + ASSERT_THAT(error.message, ::testing::HasSubstr("custom profile provider error")); + ASSERT_THAT(AdbcDatabaseRelease(&database.value, &error), IsOkStatus(&error)); +} + } // namespace adbc diff --git a/c/include/arrow-adbc/adbc.h b/c/include/arrow-adbc/adbc.h index a55f645ed7..57e665f84a 100644 --- a/c/include/arrow-adbc/adbc.h +++ b/c/include/arrow-adbc/adbc.h @@ -1300,6 +1300,11 @@ AdbcStatusCode AdbcDatabaseGetOptionInt(struct AdbcDatabase* database, const cha /// Options may be set before AdbcDatabaseInit. Some drivers may /// support setting options after initialization as well. /// +/// Driver managers may treat some option keys as manager-reserved and +/// handle them without forwarding them to the underlying driver. In +/// particular, the option key "profile" is reserved for connection +/// profiles and must not be implemented or interpreted by drivers. +/// /// \param[in] database The database. /// \param[in] key The option to set. /// \param[in] value The option value. diff --git a/c/include/arrow-adbc/adbc_driver_manager.h b/c/include/arrow-adbc/adbc_driver_manager.h index cf968ffdb4..3117f3a405 100644 --- a/c/include/arrow-adbc/adbc_driver_manager.h +++ b/c/include/arrow-adbc/adbc_driver_manager.h @@ -175,6 +175,183 @@ AdbcStatusCode AdbcDriverManagerDatabaseSetAdditionalSearchPathList( ADBC_EXPORT const char* AdbcStatusCodeMessage(AdbcStatusCode code); +/// \defgroup adbc-driver-manager-connection-profile Connection Profiles +/// Similar to odbc.ini, the ADBC driver manager can support "connection profiles" +/// that specify a driver and options to use when connecting. This allows users to +/// specify connection information in a file or environment variable, and have the +/// driver manager load the appropriate driver and set options accordingly. +/// +/// This allows creating reusable connection configurations for sharing and distribution +/// without needing to hardcode driver names and options in application code. Profiles +/// will be loaded during DatabaseInit before attempting to initialize the driver. Any +/// options specified by the profile will be applied but will not override options +/// that have already been set using DatabaseSetOption. +/// +/// To facilitate customization, we define an interface for implementing a Connection +/// Profile object along with a provider function definition which can be set into +/// the driver manager to allow for customized profile loading. +/// +/// A profile can be specified to the Driver Manager in one of two ways, +/// which will invoke the profile provider during the call to DatabaseInit: +/// +/// 1. The "profile" option can be set using DatabaseSetOption with the name of the +/// profile to load. +/// 2. The "uri" being used can have the form "profile://" +/// +/// @{ + +/// \brief Abstract interface for connection profile providers +struct ADBC_EXPORT AdbcConnectionProfile { + /// \brief Opaque implementation-defined state. + /// This field is NULL if the profile is uninitialized/freed (but + /// it need not have a value even if the profile is initialized). + void* private_data; + + /// \brief Release the profile and perform any cleanup. + void (*release)(struct AdbcConnectionProfile* profile); + + /// \brief Get the driver to use as specified by this profile. + /// + /// It is not required that a profile specify a driver. If the options + // can be reusable across drivers, then the profile does not need to specify + /// a driver (if this provides an empty string or nullptr then the driver + /// must be defined by other means, e.g. by the driver / uri options). + /// + /// \param[in] profile The profile to query. + /// \param[out] driver_name The name of the driver to use, or NULL if not specified. + /// \param[out] error An optional location to return an error message + AdbcStatusCode (*GetDriverName)(struct AdbcConnectionProfile* profile, + const char** driver_name, struct AdbcError* error); + + /// \brief Get the string options specified by the profile + /// + /// The keys and values returned by this function are owned by the profile + /// object itself and do not need to be freed or managed by the caller. + /// They must not be accessed after calling release on the profile. + /// + /// The profile can also indicate that a value should be pulled from the environment + /// by having a value in the form `env_var(ENV_VAR_NAME)`. If the driver + /// manager encounters a value of this form, it will replace it with the actual value + /// of the environment variable `ENV_VAR_NAME` before setting the option. This + /// is only valid for option *values* not *keys*. + /// + /// \param[in] profile The profile to query. + /// \param[out] keys The keys of the options specified by the profile. + /// \param[out] values The values of the options specified by the profile. + /// \param[out] num_options The number of options specified by the profile, + /// consumers must not access keys or values beyond this count. + /// \param[out] error An optional location to return an error message + AdbcStatusCode (*GetOptions)(struct AdbcConnectionProfile* profile, const char*** keys, + const char*** values, size_t* num_options, + struct AdbcError* error); + + /// \brief Get the integer options specified by the profile + /// + /// The keys and values returned by this function are owned by the profile + /// object itself and do not need to be freed or managed by the caller. They must not be + /// accessed after calling release on the profile. + /// + /// Values returned by this function will be set using the DatabaseSetOptionInt function + /// on the database object being initialized. If the driver does not support the + /// DatabaseSetOptionInt function, then options should only be returned as strings. + /// + /// \param[in] profile The profile to query. + /// \param[out] keys The keys of the options specified by the profile. + /// \param[out] values The values of the options specified by the profile. + /// \param[out] num_options The number of options specified by the profile, + /// consumers must not access keys or values beyond this count. + /// \param[out] error An optional location to return an error message + AdbcStatusCode (*GetIntOptions)(struct AdbcConnectionProfile* profile, + const char*** keys, const int64_t** values, + size_t* num_options, struct AdbcError* error); + + /// \brief Get the double options specified by the profile + /// + /// The keys and values returned by this function are owned by the profile + /// object itself and do not need to be freed or managed by the caller. They must not be + /// accessed after calling release on the profile. + /// + /// Values returned by this function will be set using the DatabaseSetOptionDouble + /// function on the database object being initialized. If the driver does not support + /// the DatabaseSetOptionDouble function, then options should only be returned as + /// strings. + /// + /// \param[in] profile The profile to query. + /// \param[out] keys The keys of the options specified by the profile. + /// \param[out] values The values of the options specified by the profile. + /// \param[out] num_options The number of options specified by the profile, + /// consumers must not access keys or values beyond this count. + /// \param[out] error An optional location to return an error message + AdbcStatusCode (*GetDoubleOptions)(struct AdbcConnectionProfile* profile, + const char*** keys, const double** values, + size_t* num_options, struct AdbcError* error); +}; + +/// \brief Common definition for a connection profile provider +/// +/// \param[in] profile_name The name of the profile to load. This is the value of the +/// "profile" option or the profile specified in the URI. +/// \param[in] additional_search_path_list A list of additional paths to search for +/// profiles, delimited by the OS specific path list separator. +/// \param[out] out The profile to return. The caller will take ownership of the profile +/// and is responsible for calling release on it when finished. +/// \param[out] error An optional location to return an error message if necessary. +typedef AdbcStatusCode (*AdbcConnectionProfileProvider)( + const char* profile_name, const char* additional_search_path_list, + struct AdbcConnectionProfile* out, struct AdbcError* error); + +/// \brief Set a custom connection profile provider for the driver manager. +/// +/// If no provider is set, the driver manager will use a default, filesystem-based +/// provider which will look for profiles in the following locations if not given an +/// absolute path to a file: +/// +/// 1. The environment variable ADBC_PROFILE_PATH, which is a list of paths to search for +/// profiles. +/// 2. The user-level configuration directory (e.g. ~/.config/adbc/profiles on Linux). +/// +/// The filesystem-based profile looks for a file named .toml if there is +/// no extension provided, attempting to parse the toml file for the profile information. +/// If the file is found and parsed successfully, the options specified in the profile +/// which have not already been set will be set as if by DatabaseSetOption just before +/// initialization as part of DatabaseInit. +/// +/// For file-based profiles the expected format is as follows: +/// ```toml +/// version = 1 +/// driver = "driver_name" +/// +/// [options] +/// option1 = "value1" +/// option2 = 42 +/// option3 = 3.14 +/// ``` +/// +/// Boolean options will be converted to string equivalents of "true" or "false". +/// +/// \param[in] database The database to set the profile provider for. +/// \param[in] provider The profile provider to use. If NULL, the default filesystem-based +/// provider will be used if a profile is needed. +/// \param[out] error An optional location to return an error message if necessary +ADBC_EXPORT +AdbcStatusCode AdbcDriverManagerDatabaseSetProfileProvider( + struct AdbcDatabase* database, AdbcConnectionProfileProvider provider, + struct AdbcError* error); + +/// \brief Default Filesystem-based profile provider for the driver manager. +/// +/// We expose this so that consumers would be able to write a provider that falls back on +/// the default filesystem-based provider if their custom provider fails to find a +/// profile. This allows for more flexible provider implementations that can still +/// leverage the default behavior when needed. +ADBC_EXPORT +AdbcStatusCode AdbcFilesystemProfileProvider(const char* profile_name, + const char* additional_search_path_list, + struct AdbcConnectionProfile* out, + struct AdbcError* error); + +/// @} + #endif // ADBC_DRIVER_MANAGER_H #ifdef __cplusplus diff --git a/docs/source/format/connection_profiles.rst b/docs/source/format/connection_profiles.rst new file mode 100644 index 0000000000..7a355c1e97 --- /dev/null +++ b/docs/source/format/connection_profiles.rst @@ -0,0 +1,568 @@ +.. 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. + +================================== +Driver Manager Connection Profiles +================================== + +Overview +======== + +Similar to ODBC's ``odbc.ini``, the ADBC driver manager supports **connection profiles** +that specify a driver and connection options in a reusable configuration. This allows users to: + +- Define connection information in files or environment variables +- Share connection configurations across applications +- Distribute standardized connection settings +- Avoid hardcoding driver names and credentials in application code + +Profiles are loaded during ``AdbcDatabaseInit()`` before initializing the driver. Options +from the profile are applied automatically but do not override options already set via ``AdbcDatabaseSetOption()``. + +Quick Start +=========== + +Using a Profile via URI +----------------------- + +The simplest way to use a profile is through a URI: + +.. code-block:: c + + AdbcDatabase database; + AdbcDatabaseNew(&database, &error); + AdbcDatabaseSetOption(&database, "uri", "profile://my_snowflake_prod", &error); + AdbcDatabaseInit(&database, &error); + +Using a Profile via Option +--------------------------- + +Alternatively, specify the profile name directly: + +.. code-block:: c + + AdbcDatabase database; + AdbcDatabaseNew(&database, &error); + AdbcDatabaseSetOption(&database, "profile", "my_snowflake_prod", &error); + AdbcDatabaseInit(&database, &error); + +Profile File Format +=================== + +Filesystem-based profiles use TOML format with the following structure: + +.. code-block:: toml + + version = 1 + driver = "snowflake" + + [options] + # String options + adbc.snowflake.sql.account = "mycompany" + adbc.snowflake.sql.warehouse = "COMPUTE_WH" + adbc.snowflake.sql.database = "PRODUCTION" + adbc.snowflake.sql.schema = "PUBLIC" + + # Integer options + adbc.snowflake.sql.client_session_keep_alive_heartbeat_frequency = 3600 + + # Double options + adbc.snowflake.sql.client_timeout = 30.5 + + # Boolean options (converted to "true" or "false" strings) + adbc.snowflake.sql.client_session_keep_alive = true + +version +------- + +- **Required**: Yes +- **Type**: Integer +- **Supported values**: ``1`` + +The ``version`` field specifies the profile format version. Currently, only version 1 is supported. +This will enable future changes while maintaining backward compatibility. + +driver +------ + +- **Required**: No +- **Type**: String + +The ``driver`` field specifies which ADBC driver to load. This can be: + +- A driver name (e.g., ``"snowflake"``) +- A path to a shared library (e.g., ``"/usr/local/lib/libadbc_driver_snowflake.so"``) +- A path to a driver manifest (e.g., ``"/etc/adbc/drivers/snowflake.toml"``) + +If omitted, the driver must be specified through other means (e.g., the ``driver`` option or ``uri`` parameter). +The driver will be loaded identically to if it was specified via ``AdbcDatabaseSetOption("driver", "")``. +For more detils, see :doc:`driver_manifests`. + +Options Section +--------------- + +The ``[options]`` section contains driver-specific configuration options. Options can be of the following types: + +**String values** + Applied using ``AdbcDatabaseSetOption()`` + + .. code-block:: toml + + adbc.snowflake.sql.account = "mycompany" + adbc.snowflake.sql.warehouse = "COMPUTE_WH" + +**Integer values** + Applied using ``AdbcDatabaseSetOptionInt()`` + + .. code-block:: toml + + adbc.snowflake.sql.client_session_keep_alive_heartbeat_frequency = 3600 + +**Double values** + Applied using ``AdbcDatabaseSetOptionDouble()`` + + .. code-block:: toml + + adbc.snowflake.sql.client_timeout = 30.5 + +**Boolean values** + Converted to strings ``"true"`` or ``"false"`` and applied using ``AdbcDatabaseSetOption()`` + + .. code-block:: toml + + adbc.snowflake.sql.client_session_keep_alive = true + +Environment Variable Substitution +---------------------------------- + +Profile values can reference environment variables using the ``env_var()`` syntax: + +.. code-block:: toml + + version = 1 + driver = "adbc_driver_snowflake" + + [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. + +.. important:: + Environment variable substitution only applies to option **values**, not **keys**. + +Profile Search Locations +========================= + +When using a profile name (not an absolute path), the driver manager searches for ``.toml`` in the following locations: + +1. **Additional Search Paths** (if configured via ``AdbcDriverManagerDatabaseSetAdditionalSearchPathList()``) +2. **ADBC_PROFILE_PATH** environment variable (colon-separated on Unix, semicolon-separated on Windows) +3. **Conda Environment** (if built with Conda support and ``CONDA_PREFIX`` is set): + - ``$CONDA_PREFIX/etc/adbc/profiles/`` +4. **User Configuration Directory**: + - Linux: ``~/.config/adbc/profiles/`` + - macOS: ``~/Library/Application Support/ADBC/Profiles/`` + - Windows: ``%LOCALAPPDATA%\ADBC\Profiles\`` + +The driver manager searches locations in order and uses the first matching profile file found. + +Using Absolute Paths +-------------------- + +To specify an absolute path to a profile file: + +.. code-block:: c + + // Via profile option + AdbcDatabaseSetOption(&database, "profile", "/etc/adbc/profiles/production.toml", &error); + + // Via URI (must have .toml extension) + AdbcDatabaseSetOption(&database, "uri", "profile:///etc/adbc/profiles/production.toml", &error); + +Examples +======== + +Example 1: Snowflake Production Profile +---------------------------------------- + +File: ``~/.config/adbc/profiles/snowflake_prod.toml`` + +.. code-block:: toml + + version = 1 + driver = "snowflake" + + [options] + adbc.snowflake.sql.account = "env_var(SNOWFLAKE_ACCOUNT)" + adbc.snowflake.sql.auth_token = "env_var(SNOWFLAKE_TOKEN)" + adbc.snowflake.sql.warehouse = "PRODUCTION_WH" + adbc.snowflake.sql.database = "PROD_DB" + adbc.snowflake.sql.schema = "PUBLIC" + adbc.snowflake.sql.client_session_keep_alive = true + adbc.snowflake.sql.client_session_keep_alive_heartbeat_frequency = 3600 + +Usage: + +.. code-block:: c + + // Set environment variables + setenv("SNOWFLAKE_ACCOUNT", "mycompany", 1); + setenv("SNOWFLAKE_TOKEN", "secret_token", 1); + + // Use profile + AdbcDatabase database; + AdbcDatabaseNew(&database, &error); + AdbcDatabaseSetOption(&database, "uri", "profile://snowflake_prod", &error); + AdbcDatabaseInit(&database, &error); + +Example 2: PostgreSQL Development Profile +------------------------------------------ + +File: ``~/.config/adbc/profiles/postgres_dev.toml`` + +.. code-block:: toml + + version = 1 + driver = "postgresql" + + [options] + uri = "postgresql://localhost:5432/dev_db?sslmode=disable" + username = "dev_user" + password = "env_var(POSTGRES_DEV_PASSWORD)" + +Example 3: Driver-Agnostic Profile +----------------------------------- + +Profiles can omit the driver field for reusable configurations: + +File: ``~/.config/adbc/profiles/default_timeouts.toml`` + +.. code-block:: toml + + version = 1 + # No driver specified - can be used with any driver + + [options] + adbc.connection.timeout = 30.0 + adbc.statement.timeout = 60.0 + +Usage (driver specified separately): + +.. code-block:: c + + AdbcDatabase database; + AdbcDatabaseNew(&database, &error); + AdbcDatabaseSetOption(&database, "driver", "adbc_driver_snowflake", &error); + AdbcDatabaseSetOption(&database, "profile", "default_timeouts", &error); + AdbcDatabaseInit(&database, &error); + +Advanced Usage +============== + +Option Precedence +----------------- + +Options are applied in the following order (later overrides earlier): + +1. Driver defaults +2. Profile options (from ``[options]`` section) +3. Options set via ``AdbcDatabaseSetOption()`` before ``AdbcDatabaseInit()`` + +Example: + +.. code-block:: c + + AdbcDatabase database; + AdbcDatabaseNew(&database, &error); + + // Profile sets warehouse = "COMPUTE_WH" + AdbcDatabaseSetOption(&database, "profile", "snowflake_prod", &error); + + // This overrides the profile setting + AdbcDatabaseSetOption(&database, "adbc.snowflake.sql.warehouse", "ANALYTICS_WH", &error); + + AdbcDatabaseInit(&database, &error); + // Result: warehouse = "ANALYTICS_WH" + +Custom Profile Providers +========================= + +Applications can implement custom profile providers to load profiles from alternative sources (databases, key vaults, configuration services, etc.). + +Interface Definition +-------------------- + +A profile provider must implement the ``AdbcConnectionProfile`` interface: + +.. code-block:: c + + struct AdbcConnectionProfile { + void* private_data; + void (*release)(struct AdbcConnectionProfile* profile); + AdbcStatusCode (*GetDriverName)(struct AdbcConnectionProfile* profile, + const char** driver_name, + struct AdbcError* error); + AdbcStatusCode (*GetOptions)(struct AdbcConnectionProfile* profile, + const char*** keys, const char*** values, + size_t* num_options, struct AdbcError* error); + AdbcStatusCode (*GetIntOptions)(struct AdbcConnectionProfile* profile, + const char*** keys, const int64_t** values, + size_t* num_options, struct AdbcError* error); + AdbcStatusCode (*GetDoubleOptions)(struct AdbcConnectionProfile* profile, + const char*** keys, const double** values, + size_t* num_options, struct AdbcError* error); + }; + +Provider Function +----------------- + +The provider function signature: + +.. code-block:: c + + typedef AdbcStatusCode (*AdbcConnectionProfileProvider)( + const char* profile_name, + const char* additional_search_path_list, + struct AdbcConnectionProfile* out, + struct AdbcError* error); + +Example Implementation +---------------------- + +.. code-block:: c + + // Example: Load profiles from a key-value store + AdbcStatusCode MyCustomProfileProvider(const char* profile_name, + const char* additional_search_path_list, + struct AdbcConnectionProfile* out, + struct AdbcError* error) { + // Fetch profile from custom source + MyProfileData* data = LoadProfileFromKeyVault(profile_name); + if (!data) { + SetError(error, "Profile not found in key vault"); + return ADBC_STATUS_NOT_FOUND; + } + + std::memset(out, 0, sizeof(struct AdbcConnectionProfile)); + // Populate profile structure + out->private_data = data; + out->release = MyProfileRelease; + out->GetDriverName = MyGetDriverName; + out->GetOptions = MyGetOptions; + out->GetIntOptions = MyGetIntOptions; + out->GetDoubleOptions = MyGetDoubleOptions; + + return ADBC_STATUS_OK; + } + + // Register custom provider + AdbcDatabase database; + AdbcDatabaseNew(&database, &error); + AdbcDriverManagerDatabaseSetProfileProvider(&database, MyCustomProfileProvider, &error); + AdbcDatabaseSetOption(&database, "profile", "prod_config", &error); + AdbcDatabaseInit(&database, &error); + +Use Cases +========= + +Development vs. Production +--------------------------- + +Maintain separate profiles for different environments: + +.. code-block:: bash + + # Development + export ADBC_PROFILE=snowflake_dev + + # Production + export ADBC_PROFILE=snowflake_prod + +Application code: + +.. code-block:: c + + const char* profile = getenv("ADBC_PROFILE"); + if (!profile) profile = "default"; + + AdbcDatabaseSetOption(&database, "profile", profile, &error); + +Credential Management +--------------------- + +Store credentials separately from code: + +.. code-block:: toml + + [options] + adbc.snowflake.sql.account = "mycompany" + adbc.snowflake.sql.auth_token = "env_var(SNOWFLAKE_TOKEN)" + +Then set ``SNOWFLAKE_TOKEN`` via environment variable, secrets manager, or configuration service. + +Multi-Tenant Applications +-------------------------- + +Use profiles to support different customer configurations: + +.. code-block:: c + + char profile_name[256]; + snprintf(profile_name, sizeof(profile_name), "customer_%s", customer_id); + + AdbcDatabaseSetOption(&database, "profile", profile_name, &error); + +Testing +------- + +Use profiles to switch between mock and real databases: + +.. code-block:: c + + #ifdef TESTING + const char* profile = "mock_database"; + #else + const char* profile = "production"; + #endif + + AdbcDatabaseSetOption(&database, "profile", profile, &error); + +Error Handling +============== + +Profile Not Found +----------------- + +If a profile cannot be found, ``AdbcDatabaseInit()`` returns ``ADBC_STATUS_NOT_FOUND`` with a detailed error message listing all searched locations: + +.. code-block:: text + + [Driver Manager] Profile not found: my_profile + Also searched these paths for profiles: + ADBC_PROFILE_PATH: /custom/path + user config dir: /home/user/.config/adbc/profiles + system config dir: /etc/adbc/profiles + +Invalid Profile Format +---------------------- + +If a profile file exists but is malformed, ``AdbcDatabaseInit()`` returns ``ADBC_STATUS_INVALID_ARGUMENT``: + +.. code-block:: text + + [Driver Manager] Could not open profile. Error at line 5: expected '=' after key. + Profile: /home/user/.config/adbc/profiles/my_profile.toml + +Missing Driver +-------------- + +If a profile doesn't specify a driver and none is provided via other means: + +.. code-block:: text + + [Driver Manager] Must provide 'driver' parameter + (or encode driver in 'uri' parameter) + +Best Practices +============== + +1. **Use environment variables for secrets**: Never store credentials directly in profile files. + + .. code-block:: toml + + # Good + password = "env_var(DB_PASSWORD)" + + # Bad + password = "my_secret_password" + +2. **Organize profiles hierarchically**: Group related profiles in subdirectories using additional search paths. + +3. **Document profile schemas**: Maintain documentation of required environment variables for each profile. + +4. **Version control without secrets**: Profile files can be version controlled when using ``env_var()`` for sensitive values. + +5. **Test profile loading**: Verify profiles load correctly in CI/CD pipelines. + +6. **Use meaningful names**: Name profiles descriptively (e.g., ``snowflake_prod_analytics`` vs. ``profile1``). + +7. **Validate environment variables**: Check that required environment variables are set before calling ``AdbcDatabaseInit()``. + +API Reference +============= + +Setting a Profile Provider +--------------------------- + +.. code-block:: c + + AdbcStatusCode AdbcDriverManagerDatabaseSetProfileProvider( + struct AdbcDatabase* database, + AdbcConnectionProfileProvider provider, + struct AdbcError* error); + +Sets a custom connection profile provider. Must be called before ``AdbcDatabaseInit()``. + +**Parameters:** + +- ``database``: Database object to configure +- ``provider``: Profile provider function, or ``NULL`` for default filesystem provider +- ``error``: Optional error output + +**Returns:** ``ADBC_STATUS_OK`` on success, error code otherwise. + +Setting Additional Search Paths +-------------------------------- + +.. code-block:: c + + AdbcStatusCode AdbcDriverManagerDatabaseSetAdditionalSearchPathList( + struct AdbcDatabase* database, + const char* path_list, + struct AdbcError* error); + +Adds additional directories to search for profiles. Must be called before ``AdbcDatabaseInit()``. + +**Parameters:** + +- ``database``: Database object to configure +- ``path_list``: OS-specific path separator delimited list (``:``) on Unix, ``;`` on Windows), or ``NULL`` to clear +- ``error``: Optional error output + +**Returns:** ``ADBC_STATUS_OK`` on success, error code otherwise. + +**Example:** + +.. code-block:: c + + // Unix/Linux/macOS + AdbcDriverManagerDatabaseSetAdditionalSearchPathList( + &database, "/opt/app/profiles:/etc/app/profiles", &error); + + // Windows + AdbcDriverManagerDatabaseSetAdditionalSearchPathList( + &database, "C:\\App\\Profiles;C:\\ProgramData\\App\\Profiles", &error); + + +See Also +======== + +- :doc:`how_manager` - Driver Manager overview +- :doc:`specification` - Driver specification and options +- :doc:`../cpp/driver_manager` - CPP Driver Manager Reference diff --git a/docs/source/format/driver_manifests.rst b/docs/source/format/driver_manifests.rst index f2d46137a4..1a48060a83 100644 --- a/docs/source/format/driver_manifests.rst +++ b/docs/source/format/driver_manifests.rst @@ -438,6 +438,13 @@ to control which directories will be searched for manifests, with the behavior b * ``LOAD_FLAG_ALLOW_RELATIVE_PATHS`` - allow a relative path to be used * ``LOAD_FLAG_DEFAULT`` - default value with all flags set +.. warning:: A driver manifest file must not be named ``profile.toml``. + The name ``profile`` is reserved for + :doc:`connection profiles `. + Using it as the basename of a driver manifest conflicts with how + driver managers interpret URIs beginning with the + ``profile://`` scheme. + Unix-like Platforms ^^^^^^^^^^^^^^^^^^^ diff --git a/docs/source/index.rst b/docs/source/index.rst index 80ecb7519f..5d4b8966ff 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -257,6 +257,7 @@ Why ADBC? format/comparison format/how_manager format/driver_manifests + format/connection_profiles format/related_work .. toctree:: diff --git a/go/adbc/drivermgr/adbc_driver_manager.cc b/go/adbc/drivermgr/adbc_driver_manager.cc index 9397b142f5..d15252b5dc 100644 --- a/go/adbc/drivermgr/adbc_driver_manager.cc +++ b/go/adbc/drivermgr/adbc_driver_manager.cc @@ -46,6 +46,7 @@ #include #include #include +#include #include #include #include @@ -65,6 +66,7 @@ std::filesystem::path InternalAdbcSystemConfigDir(); struct ParseDriverUriResult { std::string_view driver; std::optional uri; + std::optional profile; }; ADBC_EXPORT @@ -87,16 +89,32 @@ enum class SearchPathSource { kOtherError, }; +enum class SearchPathType { + kManifest, + kProfile, +}; + using SearchPaths = std::vector>; -void AddSearchPathsToError(const SearchPaths& search_paths, std::string& error_message) { +void AddSearchPathsToError(const SearchPaths& search_paths, const SearchPathType& type, + std::string& error_message) { if (!search_paths.empty()) { - error_message += "\nAlso searched these paths for manifests:"; + error_message += "\nAlso searched these paths for"; + if (type == SearchPathType::kManifest) { + error_message += " manifests:"; + } else if (type == SearchPathType::kProfile) { + error_message += " profiles:"; + } + for (const auto& [source, path] : search_paths) { error_message += "\n\t"; switch (source) { case SearchPathSource::kEnv: - error_message += "ADBC_DRIVER_PATH: "; + if (type == SearchPathType::kManifest) { + error_message += "ADBC_DRIVER_PATH: "; + } else if (type == SearchPathType::kProfile) { + error_message += "ADBC_PROFILE_PATH: "; + } break; case SearchPathSource::kUser: error_message += "user config dir: "; @@ -234,7 +252,7 @@ void SetError(struct AdbcError* error, struct AdbcError* src_error) { if (src_error->message) { size_t message_size = strlen(src_error->message); - error->message = new char[message_size]; + error->message = new char[message_size + 1]; // +1 to include null std::memcpy(error->message, src_error->message, message_size); error->message[message_size] = '\0'; } else { @@ -398,6 +416,59 @@ AdbcStatusCode LoadDriverFromRegistry(HKEY root, const std::wstring& driver_name } #endif // _WIN32 +#define CHECK_STATUS(EXPR) \ + if (auto _status = (EXPR); _status != ADBC_STATUS_OK) { \ + return _status; \ + } + +AdbcStatusCode ProcessProfileValue(std::string_view value, std::string& out, + struct AdbcError* error) { + if (value.empty()) { + SetError(error, "Profile value is null"); + return ADBC_STATUS_INVALID_ARGUMENT; + } + + const auto pos = value.find("env_var("); + if (pos == std::string_view::npos || pos != 0) { + out = std::string(value); + return ADBC_STATUS_OK; + } + + if (value[value.size() - 1] != ')') { + SetError(error, "Malformed env_var() profile value: missing closing parenthesis"); + return ADBC_STATUS_INVALID_ARGUMENT; + } + + // Extract the environment variable name from the value + // which should be formatted as env_var(VAR_NAME) as we confirmed + // above. + const auto env_var_name = value.substr(8, value.size() - 9); +#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); + out = Utf8Encode(wvalue); +#else + const char* env_value = std::getenv(std::string(env_var_name).c_str()); + if (!env_value) { + out = ""; + return ADBC_STATUS_OK; + } + out = std::string(env_value); +#endif + return ADBC_STATUS_OK; +} + /// \return ADBC_STATUS_NOT_FOUND if the manifest does not contain a driver /// path for this platform, ADBC_STATUS_INVALID_ARGUMENT if the manifest /// could not be parsed, ADBC_STATUS_OK otherwise (`info` will be populated) @@ -520,8 +591,10 @@ SearchPaths GetEnvPaths(const char_type* env_var) { #ifdef _WIN32 static const wchar_t* kAdbcDriverPath = L"ADBC_DRIVER_PATH"; +static const wchar_t* kAdbcProfilePath = L"ADBC_PROFILE_PATH"; #else static const char* kAdbcDriverPath = "ADBC_DRIVER_PATH"; +static const char* kAdbcProfilePath = "ADBC_PROFILE_PATH"; #endif // _WIN32 SearchPaths GetSearchPaths(const AdbcLoadFlags levels) { @@ -728,7 +801,7 @@ struct ManagedLibrary { extra_debug_info.end()); if (intermediate_error.error.message) { std::string error_message = intermediate_error.error.message; - AddSearchPathsToError(search_paths, error_message); + AddSearchPathsToError(search_paths, SearchPathType::kManifest, error_message); SetError(error, std::move(error_message)); } return status; @@ -947,7 +1020,7 @@ struct ManagedLibrary { error_message += "\n"; error_message += message; } - AddSearchPathsToError(attempted_paths, error_message); + AddSearchPathsToError(attempted_paths, SearchPathType::kManifest, error_message); SetError(error, error_message); return ADBC_STATUS_NOT_FOUND; } else { @@ -1000,7 +1073,7 @@ struct ManagedLibrary { error_message += "\n"; error_message += message; } - AddSearchPathsToError(attempted_paths, error_message); + AddSearchPathsToError(attempted_paths, SearchPathType::kManifest, error_message); SetError(error, error_message); return ADBC_STATUS_NOT_FOUND; } @@ -1042,6 +1115,262 @@ struct ManagedLibrary { #endif // defined(_WIN32) }; +struct FilesystemProfile { + std::filesystem::path path; + std::string driver; + std::unordered_map options; + std::unordered_map int_options; + std::unordered_map double_options; + + std::vector options_keys; + std::vector options_values; + + std::vector int_option_keys; + std::vector int_option_values; + + std::vector double_option_keys; + std::vector double_option_values; + + static void populate_connection_profile(FilesystemProfile&& profile, + struct AdbcConnectionProfile* out) { + profile.options_keys.reserve(profile.options.size()); + profile.options_values.reserve(profile.options.size()); + for (const auto& [key, value] : profile.options) { + profile.options_keys.push_back(key.c_str()); + profile.options_values.push_back(value.c_str()); + } + + profile.int_option_keys.reserve(profile.int_options.size()); + profile.int_option_values.reserve(profile.int_options.size()); + for (const auto& [key, value] : profile.int_options) { + profile.int_option_keys.push_back(key.c_str()); + profile.int_option_values.push_back(value); + } + + profile.double_option_keys.reserve(profile.double_options.size()); + profile.double_option_values.reserve(profile.double_options.size()); + for (const auto& [key, value] : profile.double_options) { + profile.double_option_keys.push_back(key.c_str()); + profile.double_option_values.push_back(value); + } + + out->private_data = new FilesystemProfile(std::move(profile)); + out->release = [](AdbcConnectionProfile* profile) { + if (!profile || !profile->private_data) { + return; + } + + delete static_cast(profile->private_data); + profile->private_data = nullptr; + profile->release = nullptr; + }; + + out->GetDriverName = [](AdbcConnectionProfile* profile, const char** out, + struct AdbcError* error) -> AdbcStatusCode { + if (!profile || !profile->private_data) { + SetError(error, "Invalid connection profile"); + return ADBC_STATUS_INVALID_ARGUMENT; + } + + auto* fs_profile = static_cast(profile->private_data); + *out = fs_profile->driver.c_str(); + return ADBC_STATUS_OK; + }; + + out->GetOptions = [](AdbcConnectionProfile* profile, const char*** keys, + const char*** values, size_t* num_options, + struct AdbcError* error) -> AdbcStatusCode { + if (!profile || !profile->private_data) { + SetError(error, "Invalid connection profile"); + return ADBC_STATUS_INVALID_ARGUMENT; + } + + if (!keys || !values || !num_options) { + SetError(error, "Output parameters cannot be null"); + return ADBC_STATUS_INVALID_ARGUMENT; + } + + auto* fs_profile = static_cast(profile->private_data); + *num_options = fs_profile->options.size(); + *keys = fs_profile->options_keys.data(); + *values = fs_profile->options_values.data(); + return ADBC_STATUS_OK; + }; + + out->GetIntOptions = [](AdbcConnectionProfile* profile, const char*** keys, + const int64_t** values, size_t* num_options, + struct AdbcError* error) -> AdbcStatusCode { + if (!profile || !profile->private_data) { + SetError(error, "Invalid connection profile"); + return ADBC_STATUS_INVALID_ARGUMENT; + } + + if (!keys || !values || !num_options) { + SetError(error, "Output parameters cannot be null"); + return ADBC_STATUS_INVALID_ARGUMENT; + } + + auto* fs_profile = static_cast(profile->private_data); + *num_options = fs_profile->int_options.size(); + *keys = fs_profile->int_option_keys.data(); + *values = fs_profile->int_option_values.data(); + return ADBC_STATUS_OK; + }; + + out->GetDoubleOptions = [](AdbcConnectionProfile* profile, const char*** keys, + const double** values, size_t* num_options, + struct AdbcError* error) -> AdbcStatusCode { + if (!profile || !profile->private_data) { + SetError(error, "Invalid connection profile"); + return ADBC_STATUS_INVALID_ARGUMENT; + } + + if (!keys || !values || !num_options) { + SetError(error, "Output parameters cannot be null"); + return ADBC_STATUS_INVALID_ARGUMENT; + } + + auto* fs_profile = static_cast(profile->private_data); + *num_options = fs_profile->double_options.size(); + *keys = fs_profile->double_option_keys.data(); + *values = fs_profile->double_option_values.data(); + return ADBC_STATUS_OK; + }; + } +}; + +struct profileVisitor { + FilesystemProfile& profile; + const std::filesystem::path& profile_path; + struct AdbcError* error; + + bool visit_table(const std::string& prefix, toml::table& table) { + for (const auto& [key, value] : table) { + if (auto* str = value.as_string()) { + profile.options[prefix + key.data()] = str->get(); + } else if (auto* int_val = value.as_integer()) { + profile.int_options[prefix + key.data()] = int_val->get(); + } else if (auto* double_val = value.as_floating_point()) { + profile.double_options[prefix + key.data()] = double_val->get(); + } else if (auto* bool_val = value.as_boolean()) { + profile.options[prefix + key.data()] = bool_val->get() ? "true" : "false"; + } else if (value.is_table()) { + if (!visit_table(prefix + key.data() + ".", *value.as_table())) { + return false; + } + } else { + std::string message = "Unsupported value type for key '" + + std::string(key.str()) + "' in profile '" + + profile_path.string() + "'"; + SetError(error, std::move(message)); + return false; + } + } + return !error->message; + } +}; + +AdbcStatusCode LoadProfileFile(const std::filesystem::path& profile_path, + FilesystemProfile& profile, struct AdbcError* error) { + toml::table config; + try { + config = toml::parse_file(profile_path.native()); + } catch (const toml::parse_error& err) { + std::string message = "Could not open profile. "; + message += err.what(); + message += ". Profile: "; + message += profile_path.string(); + SetError(error, std::move(message)); + return ADBC_STATUS_INVALID_ARGUMENT; + } + + profile.path = profile_path; + if (!config["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)); + switch (version) { + case 1: + break; + default: { + std::string message = + "Profile version '" + std::to_string(version) + + "' is not supported by this driver manager. Profile: " + profile_path.string(); + SetError(error, std::move(message)); + return ADBC_STATUS_INVALID_ARGUMENT; + } + } + + profile.driver = config["driver"].value_or(""s); + + auto options = config.at_path("options"); + if (!options.is_table()) { + std::string message = + "Profile options is not a table in profile '" + profile_path.string() + "'"; + SetError(error, std::move(message)); + return ADBC_STATUS_INVALID_ARGUMENT; + } + + auto* options_table = options.as_table(); + profileVisitor v{profile, profile_path, error}; + if (!v.visit_table("", *options_table)) { + return ADBC_STATUS_INVALID_ARGUMENT; + } + + return ADBC_STATUS_OK; +} + +SearchPaths GetProfileSearchPaths(const char* additional_search_path_list) { + SearchPaths search_paths; + { + std::vector additional_paths; + if (additional_search_path_list) { + additional_paths = InternalAdbcParsePath(additional_search_path_list); + } + + for (const auto& path : additional_paths) { + search_paths.emplace_back(SearchPathSource::kAdditional, path); + } + } + + { + auto env_paths = GetEnvPaths(kAdbcProfilePath); + search_paths.insert(search_paths.end(), env_paths.begin(), env_paths.end()); + } + +#if ADBC_CONDA_BUILD +#ifdef _WIN32 + const wchar_t* conda_name = L"CONDA_PREFIX"; +#else + const char* conda_name = "CONDA_PREFIX"; +#endif // _WIN32 + + auto venv = GetEnvPaths(conda_name); + for (const auto& [_, venv_path] : venv) { + search_paths.emplace_back(SearchPathSource::kConda, + venv_path / "etc" / "adbc" / "profiles"); + } +#else + search_paths.emplace_back(SearchPathSource::kDisabledAtCompileTime, "Conda prefix"); +#endif // ADBC_CONDA_BUILD + +#ifdef _WIN32 + const wchar_t* profiles_dir = L"Profiles"; +#elif defined(__APPLE__) + const char* profiles_dir = "Profiles"; +#else + const char* profiles_dir = "profiles"; +#endif // defined(_WIN32) + + auto user_dir = InternalAdbcUserConfigDir().parent_path() / profiles_dir; + search_paths.emplace_back(SearchPathSource::kUser, user_dir); + return search_paths; +} + /// Hold the driver DLL and the driver release callback in the driver struct. struct ManagerDriverState { // The original release callback @@ -1417,6 +1746,7 @@ struct TempDatabase { AdbcDriverInitFunc init_func = nullptr; AdbcLoadFlags load_flags = ADBC_LOAD_FLAG_ALLOW_RELATIVE_PATHS; std::string additional_search_path_list; + AdbcConnectionProfileProvider profile_provider = nullptr; }; /// Temporary state while the database is being configured. @@ -1425,14 +1755,14 @@ struct TempConnection { std::unordered_map bytes_options; std::unordered_map int_options; std::unordered_map double_options; + AdbcConnectionProfile* connection_profile = nullptr; }; static const char kDefaultEntrypoint[] = "AdbcDriverInit"; } // namespace // Other helpers (intentionally not in an anonymous namespace so they can be tested) -ADBC_EXPORT -std::filesystem::path InternalAdbcUserConfigDir() { +ADBC_EXPORT std::filesystem::path InternalAdbcUserConfigDir() { std::filesystem::path config_dir; #if defined(_WIN32) // SHGetFolderPath is just an alias to SHGetKnownFolderPath since Vista @@ -1600,22 +1930,26 @@ std::optional InternalAdbcParseDriverUri(std::string_view std::string_view d = str.substr(0, pos); if (str.size() <= pos + 1) { - return ParseDriverUriResult{d, std::nullopt}; + return ParseDriverUriResult{d, std::nullopt, std::nullopt}; } #ifdef _WIN32 if (std::filesystem::exists(std::filesystem::path(str))) { // No scheme, just a path - return ParseDriverUriResult{str, std::nullopt}; + return ParseDriverUriResult{str, std::nullopt, std::nullopt}; } #endif if (str[pos + 1] == '/') { // scheme is also driver - return ParseDriverUriResult{d, str}; + if (d == "profile" && str.size() > pos + 2) { + // found a profile URI "profile://" + return ParseDriverUriResult{"", std::nullopt, str.substr(pos + 3)}; + } + return ParseDriverUriResult{d, str, std::nullopt}; } // driver:scheme:..... - return ParseDriverUriResult{d, str.substr(pos + 1)}; + return ParseDriverUriResult{d, str.substr(pos + 1), std::nullopt}; } // Direct implementations of API methods @@ -1672,6 +2006,19 @@ AdbcStatusCode AdbcDatabaseNew(struct AdbcDatabase* database, struct AdbcError* return ADBC_STATUS_OK; } +AdbcStatusCode AdbcDriverManagerDatabaseSetProfileProvider( + struct AdbcDatabase* database, AdbcConnectionProfileProvider provider, + struct AdbcError* error) { + if (database->private_driver) { + SetError(error, "Cannot SetProfileProvider after AdbcDatabaseInit"); + return ADBC_STATUS_INVALID_STATE; + } + + TempDatabase* args = reinterpret_cast(database->private_data); + args->profile_provider = provider; + return ADBC_STATUS_OK; +} + AdbcStatusCode AdbcDatabaseGetOption(struct AdbcDatabase* database, const char* key, char* value, size_t* length, struct AdbcError* error) { @@ -1857,6 +2204,162 @@ AdbcStatusCode AdbcDriverManagerDatabaseSetInitFunc(struct AdbcDatabase* databas return ADBC_STATUS_OK; } +AdbcStatusCode AdbcFilesystemProfileProvider(const char* profile_name, + const char* additional_search_path_list, + struct AdbcConnectionProfile* out, + struct AdbcError* error) { + if (profile_name == nullptr || strlen(profile_name) == 0) { + SetError(error, "Profile name is empty"); + return ADBC_STATUS_INVALID_ARGUMENT; + } + + if (!out) { + SetError(error, "Output profile is null"); + return ADBC_STATUS_INVALID_ARGUMENT; + } + + std::memset(out, 0, sizeof(*out)); + std::filesystem::path profile_path(profile_name); + if (profile_path.has_extension()) { + if (HasExtension(profile_path, ".toml")) { + if (!std::filesystem::exists(profile_path)) { + SetError(error, "Profile file does not exist: " + profile_path.string()); + return ADBC_STATUS_NOT_FOUND; + } + + FilesystemProfile profile; + CHECK_STATUS(LoadProfileFile(profile_path, profile, error)); + FilesystemProfile::populate_connection_profile(std::move(profile), out); + return ADBC_STATUS_OK; + } + } + + if (profile_path.is_absolute()) { + profile_path.replace_extension(".toml"); + + FilesystemProfile profile; + CHECK_STATUS(LoadProfileFile(profile_path, profile, error)); + FilesystemProfile::populate_connection_profile(std::move(profile), out); + return ADBC_STATUS_OK; + } + + SearchPaths search_paths = GetProfileSearchPaths(additional_search_path_list); + SearchPaths extra_debug_info; + for (const auto& [source, search_path] : search_paths) { + if (source == SearchPathSource::kRegistry || source == SearchPathSource::kUnset || + source == SearchPathSource::kDoesNotExist || + source == SearchPathSource::kDisabledAtCompileTime || + source == SearchPathSource::kDisabledAtRunTime || + source == SearchPathSource::kOtherError) { + continue; + } + + std::filesystem::path full_path = search_path / profile_path; + full_path.replace_extension(".toml"); + if (std::filesystem::exists(full_path)) { + OwnedError intermediate_error; + + FilesystemProfile profile; + auto status = LoadProfileFile(full_path, profile, &intermediate_error.error); + if (status == ADBC_STATUS_OK) { + FilesystemProfile::populate_connection_profile(std::move(profile), out); + return ADBC_STATUS_OK; + } else if (status == ADBC_STATUS_INVALID_ARGUMENT) { + search_paths.insert(search_paths.end(), extra_debug_info.begin(), + extra_debug_info.end()); + if (intermediate_error.error.message) { + std::string error_message = intermediate_error.error.message; + AddSearchPathsToError(search_paths, SearchPathType::kProfile, error_message); + SetError(error, std::move(error_message)); + } + return status; + } + + std::string message = "found "; + message += full_path.string(); + message += " but: "; + if (intermediate_error.error.message) { + message += intermediate_error.error.message; + } else { + message += "could not load the profile"; + } + extra_debug_info.emplace_back(SearchPathSource::kOtherError, std::move(message)); + } + } + + search_paths.insert(search_paths.end(), extra_debug_info.begin(), + extra_debug_info.end()); + std::string error_message = "Profile not found: " + std::string(profile_name); + AddSearchPathsToError(search_paths, SearchPathType::kProfile, error_message); + SetError(error, std::move(error_message)); + return ADBC_STATUS_NOT_FOUND; +} + +struct ProfileGuard { + AdbcConnectionProfile& profile; + explicit ProfileGuard(AdbcConnectionProfile& profile) : profile(profile) {} + ~ProfileGuard() { + if (profile.release) { + profile.release(&profile); + } + } +}; + +AdbcStatusCode InternalInitializeProfile(TempDatabase* args, + const std::string_view profile, + struct AdbcError* error) { + if (!args->profile_provider) { + args->profile_provider = AdbcFilesystemProfileProvider; + } + + AdbcConnectionProfile connection_profile{}; + CHECK_STATUS(args->profile_provider(profile.data(), + args->additional_search_path_list.c_str(), + &connection_profile, error)); + + ProfileGuard guard{connection_profile}; + const char* driver_name = nullptr; + CHECK_STATUS( + connection_profile.GetDriverName(&connection_profile, &driver_name, error)); + if (driver_name != nullptr && strlen(driver_name) > 0) { + args->driver = driver_name; + } + + const char** keys = nullptr; + const char** values = nullptr; + size_t num_options = 0; + const int64_t* int_values = nullptr; + const double* double_values = nullptr; + + CHECK_STATUS(connection_profile.GetOptions(&connection_profile, &keys, &values, + &num_options, error)); + for (size_t i = 0; i < num_options; ++i) { + // 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)); + args->options.try_emplace(keys[i], processed); + } + + CHECK_STATUS(connection_profile.GetIntOptions(&connection_profile, &keys, &int_values, + &num_options, error)); + for (size_t i = 0; i < num_options; ++i) { + // use try_emplace so we only add the option if there isn't + // already an option with the same name + args->int_options.try_emplace(keys[i], int_values[i]); + } + + CHECK_STATUS(connection_profile.GetDoubleOptions(&connection_profile, &keys, + &double_values, &num_options, error)); + for (size_t i = 0; i < num_options; ++i) { + // use try_emplace so we only add the option if there isn't already an option with the + // same name + args->double_options.try_emplace(keys[i], double_values[i]); + } + + return ADBC_STATUS_OK; +} + AdbcStatusCode AdbcDatabaseInit(struct AdbcDatabase* database, struct AdbcError* error) { if (!database->private_data) { SetError(error, "Must call AdbcDatabaseNew before AdbcDatabaseInit"); @@ -1864,13 +2367,25 @@ AdbcStatusCode AdbcDatabaseInit(struct AdbcDatabase* database, struct AdbcError* } TempDatabase* args = reinterpret_cast(database->private_data); if (!args->init_func) { + const auto profile_in_use = args->options.find("profile"); + if (profile_in_use != args->options.end()) { + std::string_view profile = profile_in_use->second; + CHECK_STATUS(InternalInitializeProfile(args, profile, error)); + args->options.erase("profile"); + } + const auto uri = args->options.find("uri"); if (args->driver.empty() && uri != args->options.end()) { std::string owned_uri = uri->second; auto result = InternalAdbcParseDriverUri(owned_uri); - if (result && result->uri) { - args->driver = std::string{result->driver}; - args->options["uri"] = std::string{*result->uri}; + if (result) { + if (result->uri) { + args->driver = std::string{result->driver}; + args->options["uri"] = std::string{*result->uri}; + } else if (result->profile) { + args->options.erase("uri"); + CHECK_STATUS(InternalInitializeProfile(args, *result->profile, error)); + } } } else if (!args->driver.empty() && uri == args->options.end()) { std::string owned_driver = args->driver; @@ -1879,6 +2394,8 @@ AdbcStatusCode AdbcDatabaseInit(struct AdbcDatabase* database, struct AdbcError* args->driver = std::string{result->driver}; if (result->uri) { args->options["uri"] = std::string{*result->uri}; + } else if (result->profile) { + CHECK_STATUS(InternalInitializeProfile(args, *result->profile, error)); } } } @@ -2217,6 +2734,7 @@ AdbcStatusCode AdbcConnectionInit(struct AdbcConnection* connection, SetError(error, "Database is not initialized"); return ADBC_STATUS_INVALID_ARGUMENT; } + TempConnection* args = reinterpret_cast(connection->private_data); connection->private_data = nullptr; std::unordered_map options = std::move(args->options); diff --git a/go/adbc/drivermgr/arrow-adbc/adbc.h b/go/adbc/drivermgr/arrow-adbc/adbc.h index a55f645ed7..57e665f84a 100644 --- a/go/adbc/drivermgr/arrow-adbc/adbc.h +++ b/go/adbc/drivermgr/arrow-adbc/adbc.h @@ -1300,6 +1300,11 @@ AdbcStatusCode AdbcDatabaseGetOptionInt(struct AdbcDatabase* database, const cha /// Options may be set before AdbcDatabaseInit. Some drivers may /// support setting options after initialization as well. /// +/// Driver managers may treat some option keys as manager-reserved and +/// handle them without forwarding them to the underlying driver. In +/// particular, the option key "profile" is reserved for connection +/// profiles and must not be implemented or interpreted by drivers. +/// /// \param[in] database The database. /// \param[in] key The option to set. /// \param[in] value The option value. diff --git a/go/adbc/drivermgr/arrow-adbc/adbc_driver_manager.h b/go/adbc/drivermgr/arrow-adbc/adbc_driver_manager.h index cf968ffdb4..3117f3a405 100644 --- a/go/adbc/drivermgr/arrow-adbc/adbc_driver_manager.h +++ b/go/adbc/drivermgr/arrow-adbc/adbc_driver_manager.h @@ -175,6 +175,183 @@ AdbcStatusCode AdbcDriverManagerDatabaseSetAdditionalSearchPathList( ADBC_EXPORT const char* AdbcStatusCodeMessage(AdbcStatusCode code); +/// \defgroup adbc-driver-manager-connection-profile Connection Profiles +/// Similar to odbc.ini, the ADBC driver manager can support "connection profiles" +/// that specify a driver and options to use when connecting. This allows users to +/// specify connection information in a file or environment variable, and have the +/// driver manager load the appropriate driver and set options accordingly. +/// +/// This allows creating reusable connection configurations for sharing and distribution +/// without needing to hardcode driver names and options in application code. Profiles +/// will be loaded during DatabaseInit before attempting to initialize the driver. Any +/// options specified by the profile will be applied but will not override options +/// that have already been set using DatabaseSetOption. +/// +/// To facilitate customization, we define an interface for implementing a Connection +/// Profile object along with a provider function definition which can be set into +/// the driver manager to allow for customized profile loading. +/// +/// A profile can be specified to the Driver Manager in one of two ways, +/// which will invoke the profile provider during the call to DatabaseInit: +/// +/// 1. The "profile" option can be set using DatabaseSetOption with the name of the +/// profile to load. +/// 2. The "uri" being used can have the form "profile://" +/// +/// @{ + +/// \brief Abstract interface for connection profile providers +struct ADBC_EXPORT AdbcConnectionProfile { + /// \brief Opaque implementation-defined state. + /// This field is NULL if the profile is uninitialized/freed (but + /// it need not have a value even if the profile is initialized). + void* private_data; + + /// \brief Release the profile and perform any cleanup. + void (*release)(struct AdbcConnectionProfile* profile); + + /// \brief Get the driver to use as specified by this profile. + /// + /// It is not required that a profile specify a driver. If the options + // can be reusable across drivers, then the profile does not need to specify + /// a driver (if this provides an empty string or nullptr then the driver + /// must be defined by other means, e.g. by the driver / uri options). + /// + /// \param[in] profile The profile to query. + /// \param[out] driver_name The name of the driver to use, or NULL if not specified. + /// \param[out] error An optional location to return an error message + AdbcStatusCode (*GetDriverName)(struct AdbcConnectionProfile* profile, + const char** driver_name, struct AdbcError* error); + + /// \brief Get the string options specified by the profile + /// + /// The keys and values returned by this function are owned by the profile + /// object itself and do not need to be freed or managed by the caller. + /// They must not be accessed after calling release on the profile. + /// + /// The profile can also indicate that a value should be pulled from the environment + /// by having a value in the form `env_var(ENV_VAR_NAME)`. If the driver + /// manager encounters a value of this form, it will replace it with the actual value + /// of the environment variable `ENV_VAR_NAME` before setting the option. This + /// is only valid for option *values* not *keys*. + /// + /// \param[in] profile The profile to query. + /// \param[out] keys The keys of the options specified by the profile. + /// \param[out] values The values of the options specified by the profile. + /// \param[out] num_options The number of options specified by the profile, + /// consumers must not access keys or values beyond this count. + /// \param[out] error An optional location to return an error message + AdbcStatusCode (*GetOptions)(struct AdbcConnectionProfile* profile, const char*** keys, + const char*** values, size_t* num_options, + struct AdbcError* error); + + /// \brief Get the integer options specified by the profile + /// + /// The keys and values returned by this function are owned by the profile + /// object itself and do not need to be freed or managed by the caller. They must not be + /// accessed after calling release on the profile. + /// + /// Values returned by this function will be set using the DatabaseSetOptionInt function + /// on the database object being initialized. If the driver does not support the + /// DatabaseSetOptionInt function, then options should only be returned as strings. + /// + /// \param[in] profile The profile to query. + /// \param[out] keys The keys of the options specified by the profile. + /// \param[out] values The values of the options specified by the profile. + /// \param[out] num_options The number of options specified by the profile, + /// consumers must not access keys or values beyond this count. + /// \param[out] error An optional location to return an error message + AdbcStatusCode (*GetIntOptions)(struct AdbcConnectionProfile* profile, + const char*** keys, const int64_t** values, + size_t* num_options, struct AdbcError* error); + + /// \brief Get the double options specified by the profile + /// + /// The keys and values returned by this function are owned by the profile + /// object itself and do not need to be freed or managed by the caller. They must not be + /// accessed after calling release on the profile. + /// + /// Values returned by this function will be set using the DatabaseSetOptionDouble + /// function on the database object being initialized. If the driver does not support + /// the DatabaseSetOptionDouble function, then options should only be returned as + /// strings. + /// + /// \param[in] profile The profile to query. + /// \param[out] keys The keys of the options specified by the profile. + /// \param[out] values The values of the options specified by the profile. + /// \param[out] num_options The number of options specified by the profile, + /// consumers must not access keys or values beyond this count. + /// \param[out] error An optional location to return an error message + AdbcStatusCode (*GetDoubleOptions)(struct AdbcConnectionProfile* profile, + const char*** keys, const double** values, + size_t* num_options, struct AdbcError* error); +}; + +/// \brief Common definition for a connection profile provider +/// +/// \param[in] profile_name The name of the profile to load. This is the value of the +/// "profile" option or the profile specified in the URI. +/// \param[in] additional_search_path_list A list of additional paths to search for +/// profiles, delimited by the OS specific path list separator. +/// \param[out] out The profile to return. The caller will take ownership of the profile +/// and is responsible for calling release on it when finished. +/// \param[out] error An optional location to return an error message if necessary. +typedef AdbcStatusCode (*AdbcConnectionProfileProvider)( + const char* profile_name, const char* additional_search_path_list, + struct AdbcConnectionProfile* out, struct AdbcError* error); + +/// \brief Set a custom connection profile provider for the driver manager. +/// +/// If no provider is set, the driver manager will use a default, filesystem-based +/// provider which will look for profiles in the following locations if not given an +/// absolute path to a file: +/// +/// 1. The environment variable ADBC_PROFILE_PATH, which is a list of paths to search for +/// profiles. +/// 2. The user-level configuration directory (e.g. ~/.config/adbc/profiles on Linux). +/// +/// The filesystem-based profile looks for a file named .toml if there is +/// no extension provided, attempting to parse the toml file for the profile information. +/// If the file is found and parsed successfully, the options specified in the profile +/// which have not already been set will be set as if by DatabaseSetOption just before +/// initialization as part of DatabaseInit. +/// +/// For file-based profiles the expected format is as follows: +/// ```toml +/// version = 1 +/// driver = "driver_name" +/// +/// [options] +/// option1 = "value1" +/// option2 = 42 +/// option3 = 3.14 +/// ``` +/// +/// Boolean options will be converted to string equivalents of "true" or "false". +/// +/// \param[in] database The database to set the profile provider for. +/// \param[in] provider The profile provider to use. If NULL, the default filesystem-based +/// provider will be used if a profile is needed. +/// \param[out] error An optional location to return an error message if necessary +ADBC_EXPORT +AdbcStatusCode AdbcDriverManagerDatabaseSetProfileProvider( + struct AdbcDatabase* database, AdbcConnectionProfileProvider provider, + struct AdbcError* error); + +/// \brief Default Filesystem-based profile provider for the driver manager. +/// +/// We expose this so that consumers would be able to write a provider that falls back on +/// the default filesystem-based provider if their custom provider fails to find a +/// profile. This allows for more flexible provider implementations that can still +/// leverage the default behavior when needed. +ADBC_EXPORT +AdbcStatusCode AdbcFilesystemProfileProvider(const char* profile_name, + const char* additional_search_path_list, + struct AdbcConnectionProfile* out, + struct AdbcError* error); + +/// @} + #endif // ADBC_DRIVER_MANAGER_H #ifdef __cplusplus