Skip to content

Conversation

@zeroshade
Copy link
Member

An attempt to implement the ideas as suggested in #3765 to provide the ability to specify and load connection profiles to define options for use by the driver manager.

This creates an AdbcConnectionProfile struct and defines an AdbcConnectionProfileProvider function pointer typedef to allow for customized management of profiles. This also implements a default file-based profile provider as described in #3765 (comment) which will be used if no custom provider has been set.

This allows easy expansion in the future for non-file-based connection profile providers while still implementing the easier case of using file-based profiles, including hierarchical specification for now. See the documentation comments added to adbc_driver_manager.h for the full description of the semantics and explanation.

@zeroshade
Copy link
Member Author

CC @davidhcoe

if there's anyone I missed please feel free to tag others that might be interested in looking this over and reviewing it. Once everyone is onboard with the design and ideas I'll implement the handling of these profiles for the other language bindings.

Copy link
Member

@paleolimbot paleolimbot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cool!

All just questions from me to take or leave 🙂

Comment on lines +321 to +327
/// version = 1
/// driver = "driver_name"
///
/// [options]
/// option1 = "value1"
/// option2 = 42
/// option3 = 3.14
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you want to wrap this in a top level [profile] item (like pyproject.toml's [project] or Cargo.toml's [workspace])?

Suggested change
/// version = 1
/// driver = "driver_name"
///
/// [options]
/// option1 = "value1"
/// option2 = 42
/// option3 = 3.14
/// [adbc.profile]
/// version = 1
/// driver = "driver_name"
///
/// [options]
/// option1 = "value1"
/// option2 = 42
/// option3 = 3.14

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Personally I was trying to avoid having to do that. Is there any benefit to doing so?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just seemed like a common idiom in .toml configs, perhaps to allow for future expansion (e.g., if you ever want >1 profile to live in one .toml)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i think after a lot of discussion between myself, @lidavidm and @ianmcook we came to the decision that we don't want more than 1 profile to live in one .toml file haha 😄

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Having multiple connection profiles defined in a single TOML file could potentially be nice, but it complicates the profile search procedure, and I worry that it could confuse users.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think my point was just that you have some future flexibility if you nest the configuration and if you put adbc and profile in there somewhere there's a better chance somebody who runs across this file without context will know what it is.

Comment on lines +223 to +224
AdbcStatusCode (*GetDriverName)(struct AdbcConnectionProfile* profile,
const char** driver_name, struct AdbcError* error);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
AdbcStatusCode (*GetDriverName)(struct AdbcConnectionProfile* profile,
const char** driver_name, struct AdbcError* error);
AdbcStatusCode (*GetDriverName)(struct AdbcConnectionProfile* profile,
const char** driver_name, AdbcDriverInitFunc* init_func, struct AdbcError* error);

Is there any value to allowing a profile to optionally specify the init function directly? (R could maybe use this to find R package versions of drivers)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The way the code is set up doesn't allow for this. If there is an init func set then the profile handling is currently entirely skipped and it's up to the init_func to handle everything. Given the way we envision profiles working, I don't think that it makes sense for the profile to set or specify the init function.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you'd just set args->init_func instead of args->driver (understood if you'd prefer not to)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I personally prefer not, but I'm open to it if others think it's worthwhile.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you do want this while you're here and I think it is fairly easy to do. Providing an init function is a more predictable/secure way to precisely specify which driver will actually get loaded. There are now quite a lot of ways that a driver can be resolved based on name that change based on working directory/environment and if I were embedding ADBC in an application I would personally want the ability to be absolutely sure about what driver/version was loaded. (Alternatively, as an application I could invent my own way to manage Profiles and opt out of the driver manager).

@davidhcoe
Copy link
Contributor

@CurtHagenlocher will be interested in this as well.

Overview
========

Similar to ODBC's ``odbc.ini``, the ADBC driver manager supports **connection profiles**
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit, but I think we should explain this as its own thing, and cite ODBC as an inspiration further down

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed. How about we structure the beginning of this page similar to how the ADBC Driver Manager and Manifests docs page is structured? Like:

There are two ways to pass connection options to driver managers:

  1. Directly specifying connection options as arguments to driver manager functions in your application code.
  2. Referring to a connection profile which contains connection options.

(or something like that)

And we could add a section that briefly explains method 1, before continuing to the content here which covers method 2.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated the rst document. Let me know what you think

driver = "adbc_driver_snowflake"

[options]
adbc.snowflake.sql.account = "env_var(SNOWFLAKE_ACCOUNT)"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm. Why not use option = { env_var = "NAME" }?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That would prevent us from using environment variables to define substrings, which we will need for the drivers that expect their connection arguments to be in a URI.

uri = "scheme://host:port?user=user&pw=env_var(PASSWORD)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we consider using ${} instead of env_var()?

That is more concise and makes escaping literals easier (e.g. with \$ or $$).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think ${} might be a little more familiar, unless you expect to eventually have other things besides env_var.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe the expectation was that we would eventually have more special functions in addition to env_var which is why this was the original suggestion. For eventual consistency

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Upon further consideration, I think env_var is good, because it's clear and explicit, and because (as Matt says) it will be more consistent with future functions that we might want to add, for example to retrieve credentials from a secrets management tool.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You will end up with the lambda calculus in these configuration strings :)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we also still need to specify how to escape this (in the very unlikely instance that it's needed...)

@zeroshade
Copy link
Member Author

@CurtHagenlocher @davidhcoe @lidavidm @ianmcook any further comments?

@zeroshade
Copy link
Member Author

I've updated the comments and the rst document based on everyone's comments. Please re-review

@davidhcoe
Copy link
Contributor

fyi @jadewang-db and @eric-wang-1990

driver = "adbc_driver_snowflake"

[options]
adbc.snowflake.sql.account = "env_var(SNOWFLAKE_ACCOUNT)"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we also still need to specify how to escape this (in the very unlikely instance that it's needed...)

@zeroshade zeroshade force-pushed the profile-settings-configuration branch from 23fed89 to 3cebf5a Compare January 26, 2026 20:46
Comment on lines +1382 to +1384
#ifdef _WIN32
const wchar_t* profiles_dir = L"Profiles";
#elif defined(__APPLE__)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I forget whether the Windows configuration directory from InternalAdbcUserConfigDir() uses the "Roaming" or local version but it might be worth checking to make sure the Profiles are stored in the right one (i.e., should Profiles follow a user around when logging in to the same account on multiple computers, or should they be local to one computer?).

Comment on lines +223 to +224
AdbcStatusCode (*GetDriverName)(struct AdbcConnectionProfile* profile,
const char** driver_name, struct AdbcError* error);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you do want this while you're here and I think it is fairly easy to do. Providing an init function is a more predictable/secure way to precisely specify which driver will actually get loaded. There are now quite a lot of ways that a driver can be resolved based on name that change based on working directory/environment and if I were embedding ADBC in an application I would personally want the ability to be absolutely sure about what driver/version was loaded. (Alternatively, as an application I could invent my own way to manage Profiles and opt out of the driver manager).

Comment on lines +321 to +327
/// version = 1
/// driver = "driver_name"
///
/// [options]
/// option1 = "value1"
/// option2 = 42
/// option3 = 3.14
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think my point was just that you have some future flexibility if you nest the configuration and if you put adbc and profile in there somewhere there's a better chance somebody who runs across this file without context will know what it is.

Comment on lines +456 to +462
#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;
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see why this is important (to avoid putting secrets in .toml files), but there is now quite a bit of environment variable state that is required to do certain things (like prepend the driver search path with a directory of preferred drivers if I remember the order correctly), which means the chance of an application concurrently setting and the driver manager fetching an environment variable is non-trivial in the presence of something like a connection pool. Not in this PR, but it may be worth documenting how to create connections in a thread-safe way and/or ensuring there is a thread safe alternative to accomplish some of these things.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants