use percent_encoding::percent_decode_str;
use snowflake_api::SnowflakeApi;
use url::Url;
#[derive(Debug, thiserror::Error)]
pub enum SnowflakeConnectionError {
#[error("invalid Snowflake URL: {0}")]
Parse(#[from] url::ParseError),
#[error("invalid Snowflake URL: {0}")]
Config(String),
#[error("Snowflake client error: {0}")]
Client(String),
}
pub struct SnowflakeExecutor {
api: SnowflakeApi,
}
impl SnowflakeExecutor {
pub fn from_url(raw: &str) -> Result<Self, SnowflakeConnectionError> {
let parsed = Url::parse(raw)?;
let account = parsed
.host_str()
.ok_or_else(|| SnowflakeConnectionError::Config(
"missing account identifier (host part of URL)".into(),
))?
.to_owned();
let username = {
let u = parsed.username();
if u.is_empty() {
return Err(SnowflakeConnectionError::Config("missing username".into()));
}
decode(u)
};
let password = decode(
parsed
.password()
.ok_or_else(|| SnowflakeConnectionError::Config("missing password".into()))?,
);
let segments: Vec<String> = parsed
.path_segments()
.into_iter()
.flatten()
.filter(|s| !s.is_empty())
.map(decode)
.collect();
let database = segments.first().map(String::as_str);
let schema = segments.get(1).map(String::as_str);
let mut warehouse = None;
let mut role = None;
for (k, v) in parsed.query_pairs() {
match k.as_ref() {
"warehouse" => warehouse = Some(v.into_owned()),
"role" => role = Some(v.into_owned()),
_ => {}
}
}
SnowflakeApi::with_password_auth(
&account,
warehouse.as_deref(),
database,
schema,
&username,
role.as_deref(),
&password,
)
.map(|api| Self { api })
.map_err(|e| SnowflakeConnectionError::Client(e.to_string()))
}
pub(super) fn api(&self) -> &SnowflakeApi {
&self.api
}
}
fn decode(s: &str) -> String {
percent_decode_str(s).decode_utf8_lossy().into_owned()
}