use sql_dialect_fmt_parser::{parse_with_dialect, Dialect, SyntaxKind};
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",
"VACUUM `db`.`tbl` RETAIN 24 HOURS",
"OPTIMIZE t",
"OPTIMIZE main.default.events",
"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 PARTITION (dt) SELECT a, dt 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)",
"INSERT INTO t SELECT * FROM s",
"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",
"MERGE INTO t USING s ON t.id = s.id WHEN MATCHED THEN UPDATE SET t.x = s.x",
"MERGE INTO t USING s ON t.id = s.id WHEN NOT MATCHED THEN INSERT (id) VALUES (s.id)",
"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",
"CACHE LAZY TABLE t OPTIONS ('storageLevel' 'MEMORY_ONLY') SELECT a FROM s WHERE a > 0",
"UNCACHE TABLE t",
"UNCACHE TABLE IF EXISTS t",
"UNCACHE TABLE main.default.events",
"REFRESH TABLE t",
"REFRESH t",
"REFRESH '/mnt/data/events'",
"REFRESH TABLE main.default.events",
"DESCRIBE HISTORY t",
"DESC HISTORY t",
"DESCRIBE HISTORY main.default.events",
"DESCRIBE HISTORY '/mnt/data/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",
];
fn has_node(sql: &str, kind: SyntaxKind) -> bool {
parse_with_dialect(sql, Dialect::Databricks)
.syntax()
.descendants()
.any(|node| node.kind() == kind)
}
#[test]
fn all_cases_parse_clean_and_round_trip() {
for sql in CASES {
let parsed = parse_with_dialect(sql, Dialect::Databricks);
assert_eq!(
parsed.syntax().to_string(),
*sql,
"Databricks parse tree must round-trip for {sql:?}"
);
assert!(
parsed.errors().is_empty(),
"unexpected Databricks parse errors for {sql:?}: {:?}",
parsed.errors()
);
}
}
#[test]
fn vacuum_is_structured() {
assert!(has_node("VACUUM t", SyntaxKind::VACUUM_STMT));
assert!(has_node(
"VACUUM t RETAIN 0 HOURS DRY RUN",
SyntaxKind::VACUUM_STMT
));
}
#[test]
fn vacuum_no_longer_mis_splits() {
let root = parse_with_dialect("VACUUM t", Dialect::Databricks).syntax();
let stmts: Vec<_> = root.children().collect();
assert_eq!(stmts.len(), 1, "VACUUM t must be a single statement");
assert_eq!(stmts[0].kind(), SyntaxKind::VACUUM_STMT);
}
#[test]
fn optimize_and_zorder_are_structured() {
assert!(has_node("OPTIMIZE t", SyntaxKind::OPTIMIZE_STMT));
assert!(has_node(
"OPTIMIZE t ZORDER BY (a, b)",
SyntaxKind::OPTIMIZE_STMT
));
assert!(has_node(
"OPTIMIZE t ZORDER BY (a, b)",
SyntaxKind::ZORDER_CLAUSE
));
assert!(has_node(
"OPTIMIZE t ZORDER BY (a, b)",
SyntaxKind::COLUMN_LIST
));
assert!(has_node("OPTIMIZE t WHERE a > 1", SyntaxKind::WHERE_CLAUSE));
}
#[test]
fn insert_overwrite_is_an_insert_stmt() {
assert!(has_node(
"INSERT OVERWRITE TABLE t SELECT * FROM s",
SyntaxKind::INSERT_STMT
));
assert!(has_node(
"INSERT OVERWRITE TABLE t PARTITION (dt = '2024-01-01') SELECT a FROM s",
SyntaxKind::INSERT_STMT
));
let root = parse_with_dialect(
"INSERT OVERWRITE TABLE t SELECT * FROM s",
Dialect::Databricks,
)
.syntax();
assert_eq!(root.children().count(), 1);
}
#[test]
fn merge_extensions_reuse_merge_nodes() {
for sql in [
"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 SOURCE THEN DELETE",
] {
assert!(has_node(sql, SyntaxKind::MERGE_STMT), "{sql}");
assert!(has_node(sql, SyntaxKind::MERGE_WHEN), "{sql}");
}
}
#[test]
fn cache_uncache_refresh_describe_history_are_structured() {
assert!(has_node("CACHE TABLE t", SyntaxKind::CACHE_STMT));
assert!(has_node(
"CACHE TABLE t AS SELECT * FROM s",
SyntaxKind::CACHE_STMT
));
assert!(has_node(
"CACHE TABLE t AS SELECT * FROM s",
SyntaxKind::SELECT_STMT
));
assert!(has_node("UNCACHE TABLE t", SyntaxKind::UNCACHE_STMT));
assert!(has_node("REFRESH TABLE t", SyntaxKind::REFRESH_STMT));
assert!(has_node(
"DESCRIBE HISTORY t",
SyntaxKind::DESCRIBE_HISTORY_STMT
));
assert!(has_node(
"DESC HISTORY t",
SyntaxKind::DESCRIBE_HISTORY_STMT
));
assert!(has_node(
"RESTORE TABLE t TO VERSION AS OF 5",
SyntaxKind::RESTORE_STMT
));
assert!(has_node(
"ANALYZE TABLE t COMPUTE STATISTICS",
SyntaxKind::ANALYZE_STMT
));
assert!(has_node(
"MSCK REPAIR TABLE t",
SyntaxKind::MSCK_REPAIR_STMT
));
}
#[test]
fn delta_commands_are_not_recognized_under_snowflake() {
for sql in CASES {
let parsed = parse_with_dialect(sql, Dialect::Snowflake);
assert_eq!(
parsed.syntax().to_string(),
*sql,
"Snowflake must round-trip losslessly for {sql:?}"
);
let has_delta_node = parsed.syntax().descendants().any(|n| {
matches!(
n.kind(),
SyntaxKind::VACUUM_STMT
| SyntaxKind::OPTIMIZE_STMT
| SyntaxKind::ZORDER_CLAUSE
| SyntaxKind::CACHE_STMT
| SyntaxKind::UNCACHE_STMT
| SyntaxKind::REFRESH_STMT
| SyntaxKind::DESCRIBE_HISTORY_STMT
| SyntaxKind::RESTORE_STMT
| SyntaxKind::ANALYZE_STMT
| SyntaxKind::MSCK_REPAIR_STMT
)
});
assert!(
!has_delta_node,
"Snowflake must not produce a Delta-command node for {sql:?}"
);
}
}
#[test]
fn vacuum_is_two_bare_statements_under_snowflake() {
let root = parse_with_dialect("VACUUM t", Dialect::Snowflake).syntax();
assert!(root
.descendants()
.all(|n| n.kind() != SyntaxKind::VACUUM_STMT));
assert_eq!(root.to_string(), "VACUUM t");
}
#[test]
fn command_words_are_plain_identifiers_in_expression_position_under_databricks() {
for sql in [
"SELECT vacuum, optimize, cache, refresh, uncache FROM t",
"SELECT history, zorder, retain FROM t",
"SELECT a FROM optimize",
] {
let parsed = parse_with_dialect(sql, Dialect::Databricks);
assert!(
parsed.errors().is_empty(),
"Databricks should treat command words as identifiers here: {sql:?} {:?}",
parsed.errors()
);
assert_eq!(parsed.syntax().to_string(), sql);
}
}