use sql_dialect_fmt_formatter::{format, FormatOptions};
use sql_dialect_fmt_lexer::tokenize_for_dialect;
use sql_dialect_fmt_parser::parse_with_dialect;
use sql_dialect_fmt_syntax::{Dialect, SyntaxKind};
fn fmt(src: &str) -> String {
format(
src,
&FormatOptions::default().with_dialect(Dialect::Databricks),
)
}
fn signature(sql: &str) -> Vec<String> {
tokenize_for_dialect(sql, Dialect::Databricks)
.tokens
.into_iter()
.filter(|t| !t.kind.is_trivia() && t.kind != SyntaxKind::SEMICOLON)
.map(|t| t.text.to_ascii_uppercase())
.collect()
}
const CASES: &[&str] = &[
"vacuum t",
"vacuum main.default.events",
"vacuum '/mnt/data/events'",
"vacuum t retain 168 hours",
"vacuum t retain 0 hours dry run",
"vacuum t dry run",
"optimize t",
"optimize t where a > 1",
"optimize t zorder by (a)",
"optimize t zorder by (a, b, c)",
"optimize t where dt = '2024-01-01' zorder by (id, ts)",
"insert overwrite table t select * from s",
"insert overwrite t select * from s",
"insert overwrite table t partition (dt = '2024-01-01') select a, b from s",
"insert overwrite table t values (1, 2), (3, 4)",
"insert overwrite table t (a, b) select a, b from s",
"insert into t (a, b) values (1, 2)",
"merge into t using s on t.id = s.id when not matched then insert *",
"merge into t using s on t.id = s.id when not matched by target then insert *",
"merge into t using s on t.id = s.id when not matched by source then delete",
"merge into t using s on t.id = s.id when not matched by source then update set t.x = 0",
"merge into t using s on t.id = s.id when matched then update set t.x = s.x when not matched then insert * when not matched by source then delete",
"merge into t using s on t.id = s.id when not matched by source and t.flag = 1 then delete",
"cache table t",
"cache lazy table t",
"cache table t options ('storageLevel' = 'DISK_ONLY')",
"cache table t as select * from s",
"cache table t select * from s",
"uncache table t",
"uncache table if exists t",
"refresh table t",
"refresh t",
"refresh '/mnt/data/events'",
"describe history t",
"desc history t",
"describe history main.default.events",
"restore table t to version as of 5",
"restore t to timestamp as of '2024-01-01'",
"analyze table t compute statistics",
"analyze table t compute statistics for columns a, b",
"msck repair table t",
"msck repair table main.default.events sync partitions",
];
#[test]
fn all_cases_parse_clean() {
for sql in CASES {
let errors = parse_with_dialect(sql, Dialect::Databricks)
.errors()
.to_vec();
assert!(
errors.is_empty(),
"Databricks parse errors for {sql:?}: {errors:?}"
);
}
}
#[test]
fn all_cases_round_trip_losslessly() {
for sql in CASES {
let tree = parse_with_dialect(sql, Dialect::Databricks)
.syntax()
.to_string();
assert_eq!(&tree, sql, "Databricks parse tree must round-trip");
}
}
#[test]
fn formatting_is_idempotent() {
for sql in CASES {
let once = fmt(sql);
assert_eq!(once, fmt(&once), "not idempotent:\n{sql}\n---\n{once}");
}
}
#[test]
fn formatted_output_is_valid_databricks_sql() {
for sql in CASES {
let formatted = fmt(sql);
let errors = parse_with_dialect(&formatted, Dialect::Databricks)
.errors()
.to_vec();
assert!(
errors.is_empty(),
"formatted output invalid under Databricks for {sql:?}: {errors:?}\n---\n{formatted}"
);
}
}
#[test]
fn formatting_preserves_tokens() {
for sql in CASES {
let formatted = fmt(sql);
assert_eq!(
signature(sql),
signature(&formatted),
"token sequence changed:\n{sql}\n---\n{formatted}"
);
}
}
#[test]
fn golden_vacuum_one_line() {
assert_eq!(fmt("vacuum t"), "VACUUM t;\n");
assert_eq!(
fmt("vacuum t retain 168 hours dry run"),
"VACUUM t RETAIN 168 HOURS DRY RUN;\n"
);
assert_eq!(
fmt("vacuum '/mnt/data/events' retain 0 hours"),
"VACUUM '/mnt/data/events' RETAIN 0 HOURS;\n"
);
}
#[test]
fn golden_optimize_with_where_and_zorder() {
assert_eq!(fmt("optimize t"), "OPTIMIZE t;\n");
assert_eq!(
fmt("optimize t where a > 1 zorder by (a, b)"),
"OPTIMIZE t\nWHERE a > 1\nZORDER BY (a, b);\n"
);
assert_eq!(
fmt("optimize t zorder by (a, b, c)"),
"OPTIMIZE t\nZORDER BY (a, b, c);\n"
);
}
#[test]
fn golden_insert_overwrite() {
assert_eq!(
fmt("insert overwrite table t select * from s"),
"INSERT OVERWRITE TABLE t\nSELECT *\nFROM s;\n"
);
assert_eq!(
fmt("insert overwrite table t partition (dt = '2024-01-01') select a, b from s"),
"INSERT OVERWRITE TABLE t PARTITION (dt = '2024-01-01')\nSELECT a, b\nFROM s;\n"
);
}
#[test]
fn golden_merge_extensions() {
assert_eq!(
fmt("merge into t using s on t.id = s.id when not matched then insert *"),
"MERGE INTO t\nUSING s\nON t.id = s.id\nWHEN NOT MATCHED THEN INSERT *;\n"
);
assert_eq!(
fmt("merge into t using s on t.id = s.id when not matched by source then delete"),
"MERGE INTO t\nUSING s\nON t.id = s.id\nWHEN NOT MATCHED BY SOURCE THEN DELETE;\n"
);
}
#[test]
fn golden_cache_uncache_refresh() {
assert_eq!(fmt("cache table t"), "CACHE TABLE t;\n");
assert_eq!(fmt("cache lazy table t"), "CACHE LAZY TABLE t;\n");
assert_eq!(
fmt("cache table t as select * from s"),
"CACHE TABLE t AS\nSELECT *\nFROM s;\n"
);
assert_eq!(
fmt("uncache table if exists t"),
"UNCACHE TABLE IF EXISTS t;\n"
);
assert_eq!(fmt("refresh table t"), "REFRESH TABLE t;\n");
}
#[test]
fn golden_describe_history() {
assert_eq!(fmt("describe history t"), "DESCRIBE HISTORY t;\n");
assert_eq!(fmt("desc history t"), "DESC HISTORY t;\n");
}
#[test]
fn golden_restore_analyze_msck() {
assert_eq!(
fmt("restore table t to version as of 5"),
"RESTORE TABLE t TO VERSION AS OF 5;\n"
);
assert_eq!(
fmt("analyze table t compute statistics for columns a, b"),
"ANALYZE TABLE t COMPUTE STATISTICS FOR COLUMNS a, b;\n"
);
assert_eq!(
fmt("msck repair table main.default.events sync partitions"),
"MSCK REPAIR TABLE main.default.events SYNC PARTITIONS;\n"
);
}
#[test]
fn snowflake_never_panics_and_round_trips() {
for sql in CASES {
let tree = parse_with_dialect(sql, Dialect::Snowflake)
.syntax()
.to_string();
assert_eq!(
&tree, sql,
"Snowflake must round-trip losslessly for {sql:?}"
);
let once = format(sql, &FormatOptions::default());
let twice = format(&once, &FormatOptions::default());
assert_eq!(
once, twice,
"Snowflake formatting must be idempotent:\n{sql}"
);
}
}