Skip to content

Commit

Permalink
feat(source create snowflake): Support format `--account {org}.{accou…
Browse files Browse the repository at this point in the history
…nt}` (#206)

- Upgrade regress dep
- Support more user-friendly `--account {org}.{account}` format when
creating Snowflake sources

## Test plan

Failure:
```
% cargo run -- source create snowflake -n testing-easier-identifier --account ASDF --user redacted --password redacted --database redacted
error creating source: Invalid account identifers given. Provide account identifiers either via `--account ${ORG_NAME}.${ACCOUNT_NAME}, or via `--organization ${ORG_NAME} --account ${ACCOUNT_NAME}`.
```

Success:

```
% cargo run -- source create snowflake -n testing-easier-identifier --account ABC123.DEF456 --user redacted --password redacted --database redacted
Source created
```
&& verifying the expected values were persisted:
```
dpm=> select source_parameters->'organization', source_parameters->'account' from source where name = 'testing-easier-identifier';
 ?column?  | ?column?
-----------+-----------
 "ABC123" | "DEF456"
(1 row)
```
  • Loading branch information
spencerwilson committed Nov 27, 2023
1 parent c5e53a5 commit c7175e8
Show file tree
Hide file tree
Showing 4 changed files with 64 additions and 24 deletions.
4 changes: 2 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ dialoguer = "0.10.4"
directories = "5.0.1"
inquire = "0.6.2"
prost = "0.11.9"
regress = "0.6.0"
regress = "0.7.1"
reqwest = { version = "0.11.18", default-features = false, features = ["json", "rustls-tls-native-roots"] }
rust-embed = { version = "6.6.1", features = ["include-exclude"] }
semver = { version = "1.0.18", features = ["serde"] }
Expand Down
50 changes: 41 additions & 9 deletions src/command/snowflake.rs
Original file line number Diff line number Diff line change
@@ -1,21 +1,53 @@
use anyhow::{bail, Result};
use serde::{Deserialize, Serialize};

// Source: https://docs.snowflake.com/en/user-guide/admin-account-identifier#organization-name
// We assume that when ^ refers to "letters" that means [a-zA-Z].
const ORG_NAME_PATTERN: &str = "[a-zA-Z][a-zA-Z0-9]*";
const ACCOUNT_NAME_PATTERN: &str = "[a-zA-Z]\\w*";

#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct OrganizationName(String);
impl std::str::FromStr for OrganizationName {
type Err = anyhow::Error;

fn from_str(s: &str) -> Result<Self, Self::Err> {
// Source: https://docs.snowflake.com/en/user-guide/admin-account-identifier#organization-name
// We assume that when ^ refers to "letters" that means [a-zA-Z].
if regress::Regex::new("^[a-zA-Z][a-zA-Z0-9]*$")
.unwrap()
.find(s)
.is_none()
{
bail!("doesn't match pattern \"^[a-zA-Z][a-zA-Z0-9]*$\"");
let pattern = format!("^{}$", ORG_NAME_PATTERN);

if regress::Regex::new(&pattern).unwrap().find(s).is_none() {
bail!("doesn't match pattern \"{}\"", pattern);
}
Ok(Self(s.to_string()))

Ok(Self(s.to_owned()))
}
}

/// If `account_name` is of the form '{org}.{account}', prefer that. Otherwise,
/// rely on both org and account name having been provided separately. If
/// neither work out, return `Err`.
///
/// See also: https://docs.snowflake.com/en/user-guide/admin-account-identifier
pub fn resolve_account_identifiers<'a>(
organization_name: Option<&'a OrganizationName>,
account_name: &'a str,
) -> Result<(OrganizationName, &'a str)> {
let combined_pattern = regress::Regex::new(&format!(
"^(?<org_name>{})[.-](?<account_name>{})$",
ORG_NAME_PATTERN, ACCOUNT_NAME_PATTERN
))
.unwrap();

if let Some(result) = combined_pattern.find(account_name) {
// SAFETY: Regex match implies that both groups matched.
let org_name = &account_name[result.named_group("org_name").unwrap()];
let account_name = &account_name[result.named_group("account_name").unwrap()];

return Ok((OrganizationName(org_name.to_owned()), account_name));
}

if let Some(org_name) = organization_name {
return Ok((org_name.to_owned(), account_name));
}

bail!("Invalid account identifers given. Provide account identifiers either via `--account ${{ORG_NAME}}.${{ACCOUNT_NAME}}`, or via `--organization ${{ORG_NAME}} --account ${{ACCOUNT_NAME}}`.")
}
32 changes: 20 additions & 12 deletions src/command/source.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,11 @@ pub enum CreateSource {
name: String,

#[arg(long, value_name = "NAME")]
organization: snowflake::OrganizationName,
organization: Option<snowflake::OrganizationName>,

/// An account identifier string like `${ORG_NAME}.${ACCOUNT_NAME}`.
/// Alternatively, you can provide the components separately via
/// `--organization ${ORG_NAME} --account ${ACCOUNT_NAME}`.
#[arg(long, value_name = "NAME")]
account: String,

Expand Down Expand Up @@ -109,17 +112,22 @@ pub async fn create(cs: &CreateSource) -> Result<()> {
user,
password,
staging_database,
} => CreateSourceInput {
name,
source_parameters: CreateSourceParameters::Snowflake {
organization: organization.to_owned(),
account,
database,
user,
authentication_method: SnowflakeAuthenticationMethod::Password { password },
staging_database,
},
},
} => {
let (organization, account) =
snowflake::resolve_account_identifiers(organization.as_ref(), account)?;

CreateSourceInput {
name,
source_parameters: CreateSourceParameters::Snowflake {
organization,
account,
database,
user,
authentication_method: SnowflakeAuthenticationMethod::Password { password },
staging_database,
},
}
}
};

let token = session::get_token()?;
Expand Down

0 comments on commit c7175e8

Please sign in to comment.