Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 18 additions & 4 deletions src/database.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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!(
Expand All @@ -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::<Result<Vec<_>, _>>()?;

// Build override options from CLI (these take precedence)
let override_options = build_database_options(
Expand Down
56 changes: 56 additions & 0 deletions tests/integration_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down