use crate::ast::NoExt;
use crate::ast::dialect::FeatureSet;
use crate::parser::Dialect;
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
pub struct Snowflake;
impl Dialect for Snowflake {
type Ext = NoExt;
fn features(&self) -> &FeatureSet {
&FeatureSet::SNOWFLAKE
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::dialect::test_support::assert_full_grammar;
use crate::dialect::{Ansi, DuckDb, MySql, Postgres, Sqlite};
use crate::parse_with;
use crate::parser::Dialect;
#[test]
fn snowflake_parses_the_full_m1_select_grammar() {
assert_full_grammar(Snowflake);
}
fn rejects_under_every_oracle_preset(sql: &str) {
assert!(
parse_with(sql, crate::ParseConfig::new(Ansi)).is_err(),
"ANSI must reject the Snowflake-only form {sql:?}",
);
assert!(
parse_with(sql, crate::ParseConfig::new(Postgres)).is_err(),
"PostgreSQL must reject the Snowflake-only form {sql:?}",
);
assert!(
parse_with(sql, crate::ParseConfig::new(MySql)).is_err(),
"MySQL must reject the Snowflake-only form {sql:?}",
);
assert!(
parse_with(sql, crate::ParseConfig::new(Sqlite)).is_err(),
"SQLite must reject the Snowflake-only form {sql:?}",
);
assert!(
parse_with(sql, crate::ParseConfig::new(DuckDb)).is_err(),
"DuckDB must reject the Snowflake-only form {sql:?}",
);
}
#[test]
fn snowflake_parses_semi_structured_paths_that_oracle_presets_reject() {
for sql in ["SELECT src:customer", "SELECT src:customer[0].name"] {
assert!(
parse_with(sql, crate::ParseConfig::new(Snowflake)).is_ok(),
"Snowflake parses {sql:?} as a semi-structured path"
);
rejects_under_the_non_duckdb_oracle_presets(sql);
assert!(
parse_with(sql, crate::ParseConfig::new(DuckDb)).is_ok(),
"DuckDB reinterprets {sql:?} as a prefix colon alias, not a path",
);
}
let mid = "SELECT a + src:customer FROM t";
assert!(
parse_with(mid, crate::ParseConfig::new(Snowflake)).is_ok(),
"Snowflake parses {mid:?}"
);
rejects_under_every_oracle_preset(mid);
}
fn rejects_under_the_non_duckdb_oracle_presets(sql: &str) {
assert!(
parse_with(sql, crate::ParseConfig::new(Ansi)).is_err(),
"ANSI must reject {sql:?}"
);
assert!(
parse_with(sql, crate::ParseConfig::new(Postgres)).is_err(),
"PostgreSQL must reject {sql:?}",
);
assert!(
parse_with(sql, crate::ParseConfig::new(MySql)).is_err(),
"MySQL must reject {sql:?}"
);
assert!(
parse_with(sql, crate::ParseConfig::new(Sqlite)).is_err(),
"SQLite must reject {sql:?}"
);
}
#[test]
fn snowflake_parses_qualify_and_group_by_all_that_the_non_duckdb_presets_reject() {
for sql in [
"SELECT a FROM t QUALIFY row_number() OVER () = 1",
"SELECT a, count(*) FROM t GROUP BY ALL",
] {
assert!(
parse_with(sql, crate::ParseConfig::new(Snowflake)).is_ok(),
"Snowflake parses {sql:?}"
);
rejects_under_the_non_duckdb_oracle_presets(sql);
assert!(
parse_with(sql, crate::ParseConfig::new(DuckDb)).is_ok(),
"DuckDB shares {sql:?} (its own QUALIFY / GROUP BY ALL)",
);
}
}
#[test]
fn snowflake_parses_copy_into_that_every_oracle_preset_rejects() {
for sql in [
"COPY INTO t FROM 's3://bucket/data/' FILE_FORMAT = (TYPE = CSV SKIP_HEADER = 1) ON_ERROR = CONTINUE",
"COPY INTO 's3://bucket/unload/' FROM t OVERWRITE = TRUE",
"COPY INTO t FROM (SELECT a FROM src) PATTERN = '.*[.]csv'",
] {
let parsed = parse_with(sql, crate::ParseConfig::new(Snowflake))
.unwrap_or_else(|err| panic!("Snowflake parses {sql:?}: {err:?}"));
let rendered = crate::render::Renderer::new(crate::parser::FeatureDialect {
features: &FeatureSet::SNOWFLAKE,
})
.render_parsed(&parsed)
.unwrap_or_else(|err| panic!("{sql:?} renders: {err:?}"));
assert_eq!(rendered, sql, "COPY INTO round-trips under Snowflake");
rejects_under_every_oracle_preset(sql);
}
}
#[test]
fn snowflake_reserves_qualify_as_an_identifier() {
assert!(
parse_with(
"SELECT a FROM t QUALIFY row_number() OVER () = 1",
crate::ParseConfig::new(Snowflake)
)
.is_ok(),
"the bare `FROM t QUALIFY …` form parses the clause, not an alias",
);
assert!(
parse_with("SELECT qualify FROM t", crate::ParseConfig::new(Snowflake)).is_err(),
"Snowflake rejects `qualify` as a column name (reserved)",
);
assert!(
parse_with("SELECT qualify FROM t", crate::ParseConfig::new(Ansi)).is_ok(),
"ANSI reads `qualify` as an ordinary column name (unreserved)",
);
}
#[test]
fn snowflake_features_are_the_snowflake_preset() {
assert_eq!(Snowflake.features(), &FeatureSet::SNOWFLAKE);
}
#[test]
fn snowflake_parses_stage_references_in_copy_into() {
use crate::ast::{CopyIntoSource, CopyIntoTarget, Statement};
for sql in [
"COPY INTO t FROM @my_stage",
"COPY INTO t FROM @my_stage/path/file.csv",
"COPY INTO t FROM @~",
"COPY INTO t FROM @~/data",
"COPY INTO t FROM @%src_table",
"COPY INTO @my_stage FROM t",
"COPY INTO @db.schema.stage/path FROM (SELECT 1)",
] {
let parsed = parse_with(sql, crate::ParseConfig::new(Snowflake))
.unwrap_or_else(|e| panic!("must parse {sql:?}: {e:?}"));
let Statement::CopyInto { copy, .. } = &parsed.statements()[0] else {
panic!("expected CopyInto for {sql:?}");
};
let has_stage = matches!(copy.target, CopyIntoTarget::Stage { .. })
|| matches!(copy.source, CopyIntoSource::Stage { .. });
assert!(has_stage, "expected a Stage endpoint in {sql:?}: {copy:?}");
let rendered = crate::render::Renderer::new(crate::parser::FeatureDialect {
features: &FeatureSet::SNOWFLAKE,
})
.render_parsed(&parsed)
.unwrap_or_else(|err| panic!("{sql:?} renders: {err:?}"));
assert_eq!(rendered, sql, "stage COPY INTO round-trips under Snowflake");
}
}
}