From 654ade0b6dc5849d0a4e2b451827f770f78eba28 Mon Sep 17 00:00:00 2001 From: theiotidiot <64869689+theiotidiot@users.noreply.github.com> Date: Thu, 25 Jun 2026 23:58:49 -0700 Subject: [PATCH] fix: expand env_var substitution in profile option values The ADBC connection-profile spec specifies that profile option values containing `{{ env_var(NAME) }}` are expanded against the process environment when the profile is loaded. `adbc_driver_manager` performs this expansion when callers go through its `DriverLocator::Profile` path (via `process_profile_value`), but databow's `initialize_profile_connection` loads the profile through `FilesystemProfileProvider` directly and forwards the resulting `OptionValue`s to the driver without applying the substitution. Profiles that reference env vars therefore fail at connect time because the literal template string reaches the driver. Apply `process_profile_value` to every `OptionValue::String` returned by the profile, matching the driver manager's own behavior. Add a regression test that asserts the expanded value reaches the driver (the test points DuckDB at a database file whose name comes from an env var and verifies the substituted path exists on disk while the literal-template path does not). Fixes #19 --- src/database.rs | 22 ++++++++++++--- tests/integration_test.rs | 56 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 74 insertions(+), 4 deletions(-) diff --git a/src/database.rs b/src/database.rs index 993780a..27bc236 100644 --- a/src/database.rs +++ b/src/database.rs @@ -5,7 +5,7 @@ use crate::cli::ConnectionSource; use adbc_core::options::{AdbcVersion, OptionDatabase, OptionValue}; use adbc_core::{Database, Driver, LOAD_FLAG_DEFAULT, Statement}; use adbc_driver_manager::profile::{ - ConnectionProfile, ConnectionProfileProvider, FilesystemProfileProvider, + ConnectionProfile, ConnectionProfileProvider, FilesystemProfileProvider, process_profile_value, }; use adbc_driver_manager::{ManagedConnection, ManagedDriver}; use arrow_array::RecordBatch; @@ -104,8 +104,9 @@ fn initialize_profile_connection( .map_err(|e| format!("Failed to load driver '{}': {}", driver_name, e))? }; - // Collect profile options - let profile_options: Vec<_> = profile + // Collect profile options, applying ADBC `{{ env_var(NAME) }}` substitution + // on string values (matches the driver manager's `DriverLocator::Profile` path). + let profile_options: Vec<(OptionDatabase, OptionValue)> = profile .get_options() .map_err(|e| { format!( @@ -114,7 +115,20 @@ fn initialize_profile_connection( ) })? .into_iter() - .collect(); + .map(|(k, v)| -> Result<(OptionDatabase, OptionValue), String> { + if let OptionValue::String(s) = v { + let result = process_profile_value(&s).map_err(|e| { + format!( + "Failed to substitute env vars in profile '{}': {}", + profile_name, e + ) + })?; + Ok((k, result)) + } else { + Ok((k, v)) + } + }) + .collect::, _>>()?; // Build override options from CLI (these take precedence) let override_options = build_database_options( diff --git a/tests/integration_test.rs b/tests/integration_test.rs index 808f5ed..4ac04ea 100644 --- a/tests/integration_test.rs +++ b/tests/integration_test.rs @@ -326,6 +326,62 @@ uri = ":memory:" assert!(stdout.contains("profile_options_test")); } +#[test] +fn test_profile_env_var_substitution() { + use std::io::Write; + + // Profile options containing `{{ env_var(NAME) }}` must be expanded against + // the process environment when the profile is loaded (per the ADBC connection-profile spec). + let tmp_dir = tempfile::tempdir().expect("create tempdir"); + let canary = "databow_env_var_substitution_canary"; + let expanded_db_path = tmp_dir.path().join(format!("{canary}.duckdb")); + let literal_db_path = tmp_dir + .path() + .join("{{ env_var(DATABOW_TEST_CANARY) }}.duckdb"); + + let mut profile_file = NamedTempFile::with_suffix(".toml").expect("create profile temp file"); + let profile_path = profile_file.path().to_string_lossy().to_string(); + let uri_template = format!( + "{}/{{{{ env_var(DATABOW_TEST_CANARY) }}}}.duckdb", + tmp_dir.path().display() + ); + let profile_contents = format!( + "profile_version = 1\ndriver = \"duckdb\"\n\n[Options]\nuri = \"{uri_template}\"\n" + ); + profile_file + .write_all(profile_contents.as_bytes()) + .expect("write profile"); + + let output = Command::new("cargo") + .args([ + "run", + "--", + "--profile", + &profile_path, + "--query", + "SELECT 'env_var_substituted' AS result", + ]) + .env("DATABOW_TEST_CANARY", canary) + .output() + .expect("Failed to execute command"); + + assert!( + output.status.success(), + "expected query to succeed after env_var substitution. stderr: {}", + String::from_utf8_lossy(&output.stderr) + ); + assert!( + expanded_db_path.exists(), + "expected DuckDB to have created the substituted path {:?}, but it does not exist; profile substitution did not occur", + expanded_db_path + ); + assert!( + !literal_db_path.exists(), + "unexpected: DuckDB created the literal-template path {:?}, which means env_var substitution did not happen", + literal_db_path + ); +} + #[test] fn test_timestamp_with_time_zone() { let output = Command::new("cargo")