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")