hinge/infrastructure/snowflake/
executor.rs1use percent_encoding::percent_decode_str;
2use snowflake_api::SnowflakeApi;
3use url::Url;
4
5#[derive(Debug, thiserror::Error)]
8pub enum SnowflakeConnectionError {
9 #[error("invalid Snowflake URL: {0}")]
10 Parse(#[from] url::ParseError),
11 #[error("invalid Snowflake URL: {0}")]
12 Config(String),
13 #[error("Snowflake client error: {0}")]
14 Client(String),
15}
16
17pub struct SnowflakeExecutor {
26 api: SnowflakeApi,
27}
28
29impl SnowflakeExecutor {
30 pub fn from_url(raw: &str) -> Result<Self, SnowflakeConnectionError> {
47 let parsed = Url::parse(raw)?;
48
49 let account = parsed
50 .host_str()
51 .ok_or_else(|| SnowflakeConnectionError::Config(
52 "missing account identifier (host part of URL)".into(),
53 ))?
54 .to_owned();
55
56 let username = {
57 let u = parsed.username();
58 if u.is_empty() {
59 return Err(SnowflakeConnectionError::Config("missing username".into()));
60 }
61 decode(u)
62 };
63
64 let password = decode(
65 parsed
66 .password()
67 .ok_or_else(|| SnowflakeConnectionError::Config("missing password".into()))?,
68 );
69
70 let segments: Vec<String> = parsed
71 .path_segments()
72 .into_iter()
73 .flatten()
74 .filter(|s| !s.is_empty())
75 .map(decode)
76 .collect();
77
78 let database = segments.first().map(String::as_str);
79 let schema = segments.get(1).map(String::as_str);
80
81 let mut warehouse = None;
82 let mut role = None;
83 for (k, v) in parsed.query_pairs() {
84 match k.as_ref() {
85 "warehouse" => warehouse = Some(v.into_owned()),
86 "role" => role = Some(v.into_owned()),
87 _ => {}
88 }
89 }
90
91 SnowflakeApi::with_password_auth(
92 &account,
93 warehouse.as_deref(),
94 database,
95 schema,
96 &username,
97 role.as_deref(),
98 &password,
99 )
100 .map(|api| Self { api })
101 .map_err(|e| SnowflakeConnectionError::Client(e.to_string()))
102 }
103
104 pub(super) fn api(&self) -> &SnowflakeApi {
105 &self.api
106 }
107}
108
109fn decode(s: &str) -> String {
110 percent_decode_str(s).decode_utf8_lossy().into_owned()
111}