use sql_dialect_fmt_parser::{
parse, parse_with_dialect, AstNode, BlockStmt, ColumnDef, ColumnDefList, CreateStmt, Dialect,
LetStmt, MergeStmt, MergeWhen, ObjectProperty, ReturnStmt, SelectStmt, SourceFile, StmtList,
SyntaxKind,
};
use sql_dialect_fmt_test_support::parser::{assert_parse_clean, assert_parse_roundtrip};
#[test]
fn lossless_roundtrip_valid_and_broken() {
let inputs = [
"",
" \n ",
"SELECT 1",
"select 1, 2 ,3",
"SELECT * FROM t",
"SELECT a, b AS x, c AS \"Y\" FROM db.sch.t",
"SELECT a, b FROM t WHERE a > 1 AND b <= 2 OR NOT c",
"SELECT count(*), sum(x), f(a, b) FROM t",
"SELECT a :: int, (a + b) * c, -a, x || y FROM t",
"SELECT arr[0], obj['k'] FROM t t_alias",
"1 + 2 * 3 ;\nSELECT 1 ;",
"/* hi */ SELECT n FROM t -- tail comment\n",
"INSERT INTO t (a, b) VALUES (1, 2), (3, 4)",
"INSERT INTO t SELECT * FROM u",
"INSERT OVERWRITE INTO t (a) VALUES (1)",
"INSERT ALL INTO a INTO b (x) SELECT c1, c2 FROM src",
"INSERT FIRST WHEN sev >= 9 THEN INTO high ELSE INTO low SELECT sev FROM events",
"UPDATE t SET a = 1, b = a + 2 FROM s WHERE id = 5",
"DELETE FROM t USING u WHERE t.id = u.id",
"MERGE INTO tgt t USING src s ON t.id = s.id WHEN MATCHED THEN UPDATE SET t.v = s.v WHEN NOT MATCHED THEN INSERT (id) VALUES (s.id)",
"CREATE OR REPLACE VIEW v AS SELECT a, b FROM t",
"CREATE TABLE t (a INT, b VARCHAR(10) NOT NULL, c NUMBER(10,2) DEFAULT 0)",
"CREATE TABLE t AS SELECT * FROM u",
"CREATE OR REPLACE PROCEDURE p(x INT) RETURNS INT LANGUAGE SQL AS $$ begin return x; end $$",
"CREATE FUNCTION add1(n FLOAT) RETURNS FLOAT AS 'n + 1'",
"SET target_table = 'MART.X'",
"SET (a, b) = (1, 2)",
"EXECUTE IMMEDIATE 'select 1'",
"EXECUTE IMMEDIATE $$ begin return 1; end $$",
"COPY INTO raw.orders FROM @raw.stage/orders/ FILE_FORMAT = (TYPE = JSON) ON_ERROR = CONTINUE",
"COPY INTO @mart.stage/out/ FROM (SELECT * FROM t) FILE_FORMAT = (TYPE = CSV) PARTITION BY (dt)",
"DROP TABLE IF EXISTS db.s.t CASCADE",
"ALTER TABLE t ADD COLUMN c INT",
"GRANT SELECT, INSERT ON TABLE db.s.t TO ROLE analyst",
"REVOKE USAGE ON WAREHOUSE wh FROM ROLE r",
"CALL db.sch.proc(1, 2, 'x')",
"SELECT listagg(x, ',') WITHIN GROUP (ORDER BY x DESC) FROM t",
"SELECT * FROM t PIVOT (sum(amount) FOR month IN ('jan', 'feb')) AS p",
"SELECT * FROM sales UNPIVOT (amount FOR quarter IN (q1, q2))",
"SELECT * FROM t MATCH_RECOGNIZE (PARTITION BY id PATTERN (a b+) DEFINE b AS b.v > 0) mr",
"SELECT * FROM t MATCH_RECOGNIZE (ORDER BY ts MEASURES FINAL LAST(v) AS final_v, RUNNING SUM(v) AS running_v PATTERN (a+) DEFINE a AS TRUE)",
"SELECT * FROM (WITH c AS (SELECT 1) SELECT * FROM c)",
"CREATE VIEW v AS WITH c AS (SELECT 1) SELECT * FROM c",
"MERGE INTO tgt USING (WITH c AS (SELECT 1 id) SELECT * FROM c) s ON tgt.id = s.id WHEN MATCHED THEN DELETE",
"SELECT * FROM t TABLESAMPLE BERNOULLI(25) REPEATABLE(99)",
"SELECT * FROM sales PIVOT (sum(amt) FOR m IN (1 AS jan, 2 AS feb)) p",
"SELECT * FROM t WHERE a IS DISTINCT FROM b",
"SELECT * FROM q ASOF JOIN t MATCH_CONDITION (q.ts >= t.ts) ON q.sym = t.sym",
"SELECT * FROM a ASOF JOIN b MATCH_CONDITION (a.t >= b.t)",
"SELECT * FROM t AT (TIMESTAMP => '2024-01-01'::timestamp)",
"SELECT * FROM orders BEFORE (STATEMENT => 'abc') o",
"BEGIN\nLET x := 1;\nRETURN x;\nEND",
"DECLARE\nv INT DEFAULT 0;\nBEGIN\nIF (v > 0) THEN\nRETURN 1;\nELSE\nRETURN 0;\nEND IF;\nEND",
"BEGIN\nFOR i IN 1 TO 3 DO\nINSERT INTO t VALUES (i);\nEND FOR;\nEND",
"BEGIN\nIF (x > 0) THEN\nRETURN 1;\nEND", "SELECT )( garbage @ # FROM", ];
for s in inputs {
assert_parse_roundtrip(s);
}
}
#[test]
fn scripting_blocks_parse_into_structured_nodes() {
let p = parse("DECLARE\nv INT DEFAULT 0;\nBEGIN\nIF (v > 0) THEN\nv := 1;\nEND IF;\nEND");
assert!(p.errors().is_empty(), "{:?}", p.errors());
let block = p
.syntax()
.children()
.find(|n| n.kind() == SyntaxKind::BLOCK_STMT)
.expect("a BLOCK_STMT");
let kinds: Vec<SyntaxKind> = block.descendants().map(|n| n.kind()).collect();
assert!(kinds.contains(&SyntaxKind::DECLARE_SECTION));
assert!(kinds.contains(&SyntaxKind::IF_STMT));
assert!(kinds.contains(&SyntaxKind::STMT_LIST));
}
#[test]
fn routine_bodies_cover_all_supported_languages_and_unquoted_scripting() {
for sql in [
"CREATE PROCEDURE js_p() RETURNS STRING NOT NULL LANGUAGE JAVASCRIPT AS -- delimiter comment\n$$ var statement = snowflake.createStatement({sqlText: \"select 1\"}); return statement.execute().next(); $$",
"CREATE PROCEDURE py_p() RETURNS STRING LANGUAGE PYTHON RUNTIME_VERSION = '3.12' PACKAGES = ('snowflake-snowpark-python') HANDLER = 'main' AS $$\ndef main(session):\n return 'ok'\n$$",
"CREATE PROCEDURE java_tabular() RETURNS TABLE(id NUMBER, name STRING) LANGUAGE JAVA RUNTIME_VERSION = '17' PACKAGES = ('com.snowflake:snowpark:latest') HANDLER = 'Proc.run' TARGET_PATH = '@stage/proc.jar' AS $$ class Proc {} $$",
"CREATE PROCEDURE scala_p() RETURNS STRING LANGUAGE SCALA RUNTIME_VERSION = '2.12' PACKAGES = ('com.snowflake:snowpark:latest') HANDLER = 'Main.run' AS $$\nclass Main { def run(session: com.snowflake.snowpark.Session): String = \"ok\" }\n$$",
"CREATE PROCEDURE sql_p() RETURNS STRING LANGUAGE SQL AS BEGIN RETURN 'ok'; END",
"CREATE PROCEDURE sql_decl_p() RETURNS NUMBER LANGUAGE SQL AS DECLARE x NUMBER DEFAULT 1; BEGIN RETURN x; END",
] {
assert_parse_clean(sql);
}
let p = parse("CREATE PROCEDURE sql_p() RETURNS STRING LANGUAGE SQL AS BEGIN RETURN 'ok'; END");
assert!(p.errors().is_empty(), "{:?}", p.errors());
assert!(
p.syntax()
.descendants()
.any(|node| node.kind() == SyntaxKind::BLOCK_STMT),
"unquoted SQL routine body should parse as a Snowflake Scripting block"
);
}
#[test]
fn statement_families_can_start_a_flow_chain() {
for sql in [
"SHOW TABLES IN SCHEMA db.s ->> SELECT \"name\" FROM $1",
"DROP TABLE t ->> SELECT * FROM $1",
"ALTER TABLE t ADD COLUMN c int ->> SELECT * FROM $1",
"GRANT SELECT ON TABLE t TO ROLE r ->> SELECT * FROM $1",
"REVOKE SELECT ON TABLE t FROM ROLE r ->> SELECT * FROM $1",
"CALL p(1) ->> SELECT * FROM $1",
"COMMENT ON TABLE t IS 'x' ->> SELECT * FROM $1",
"COPY INTO t FROM @s ->> SELECT * FROM $1",
"CREATE TABLE t (a int) ->> SELECT * FROM $1",
"CREATE WAREHOUSE wh WAREHOUSE_SIZE = 'XSMALL' ->> SELECT * FROM $1",
"CREATE SEMANTIC VIEW sv TABLES(orders AS t PRIMARY KEY(id)) ->> SELECT * FROM $1",
] {
let p = parse(sql);
assert!(p.errors().is_empty(), "{sql}: {:?}", p.errors());
assert!(
p.syntax()
.descendants()
.any(|node| node.kind() == SyntaxKind::FLOW_STMT),
"expected FLOW_STMT for {sql}: {}",
p.syntax()
);
}
}
#[test]
fn let_with_case_expression_is_not_split_at_inner_end() {
let p =
parse("BEGIN\nLET label := (CASE WHEN x > 0 THEN 'p' ELSE 'n' END);\nRETURN label;\nEND");
assert!(p.errors().is_empty(), "{:?}", p.errors());
let block = p
.syntax()
.children()
.find(|n| n.kind() == SyntaxKind::BLOCK_STMT)
.expect("a BLOCK_STMT");
let stmt_list = block
.descendants()
.find(|n| n.kind() == SyntaxKind::STMT_LIST)
.expect("a STMT_LIST");
assert_eq!(stmt_list.children().count(), 2);
}
#[test]
fn malformed_block_errors_so_formatter_keeps_it_verbatim() {
let p = parse("BEGIN\nIF (x > 0) THEN\nRETURN 1;\nEND");
assert!(!p.errors().is_empty());
}
#[test]
fn clean_sql_has_no_errors() {
for s in [
"SELECT 1",
"SELECT a, b FROM t WHERE a > 1",
"SELECT language, python, scala, sql FROM t",
"SELECT count(*) FROM db.s.t",
"SELECT a::int, (a + b) * c FROM t",
"SELECT DISTINCT a FROM t",
"SELECT count(DISTINCT x), array_agg(ALL y) FROM t",
"SELECT listagg(DISTINCT x, ',') FROM t",
"SELECT listagg(x, ',') WITHIN GROUP (ORDER BY x) FROM t",
"SELECT count(grouping(a)) FROM t",
"SELECT a FROM t GROUP BY GROUPING SETS ((a, b), (c), ())",
"SELECT a FROM t GROUP BY CUBE(a, b)",
"SELECT a FROM t GROUP BY ROLLUP(a), b",
"SELECT f.value FROM t, LATERAL FLATTEN(input => t.items) f",
"SELECT * FROM TABLE(FLATTEN(input => parse_json(x), outer => TRUE))",
"SELECT f(a => 1, b => 2) FROM t",
"GRANT SELECT, INSERT ON TABLE db.s.t TO ROLE analyst",
"GRANT SELECT (c1, c2) ON VIEW v TO ROLE reader",
"GRANT OWNERSHIP ON TABLE t TO ROLE admin COPY CURRENT GRANTS",
"REVOKE USAGE ON WAREHOUSE wh FROM ROLE r",
"CALL refresh_all()",
"CALL db.sch.load_data('2026-01-01', 42, TRUE)",
"USE ROLE sysadmin",
"USE WAREHOUSE compute_wh",
"USE SCHEMA db.analytics",
"SHOW TABLES IN SCHEMA db.s",
"DESCRIBE TABLE db.s.t",
"DESC USER u",
"TRUNCATE TABLE db.s.t",
"COMMENT ON TABLE db.s.t IS 'facts'",
"COMMENT ON COLUMN db.s.t.c IS 'a column'",
"COMMIT",
"COMMIT WORK",
"ROLLBACK",
"ROLLBACK TO SAVEPOINT sp1",
"BEGIN TRANSACTION",
"BEGIN WORK",
"BEGIN TRANSACTION NAME my_txn",
"UNDROP TABLE db.s.t",
"UNDROP SCHEMA db.s",
"SELECT a FROM t ORDER BY a DESC, b ASC",
"SELECT comment, id FROM t WHERE comment IS NOT NULL",
] {
assert_parse_clean(s);
}
}
#[test]
fn transaction_and_undrop_with_operands_are_single_statements() {
for (sql, kind) in [
("COMMIT WORK", SyntaxKind::TRANSACTION_STMT),
("ROLLBACK TO SAVEPOINT sp1", SyntaxKind::TRANSACTION_STMT),
("UNDROP SCHEMA db.s", SyntaxKind::UNDROP_STMT),
] {
let p = parse(sql);
assert!(p.errors().is_empty(), "{sql} should parse cleanly");
let stmts: Vec<_> = p.syntax().children().collect();
assert_eq!(stmts.len(), 1, "{sql} must be a single statement");
assert_eq!(stmts[0].kind(), kind);
}
}
#[test]
fn begin_transaction_parses_but_scripting_block_stays_verbatim() {
let txn = parse("BEGIN TRANSACTION");
assert!(txn.errors().is_empty());
assert!(txn
.syntax()
.children()
.any(|n| n.kind() == SyntaxKind::TRANSACTION_STMT));
let block = parse("BEGIN\nINSERT INTO t VALUES (1);\nINSERT INTO t VALUES (2);\nEND");
let begins_a_txn = block
.syntax()
.descendants()
.any(|n| n.kind() == SyntaxKind::TRANSACTION_STMT);
assert!(
!begins_a_txn,
"a scripting BEGIN ... END block must not parse as a transaction statement"
);
}
#[test]
fn comment_keyword_does_not_shadow_the_comment_identifier() {
let stmt = parse("COMMENT ON TABLE t IS 'x'");
assert!(stmt.errors().is_empty());
assert!(stmt
.syntax()
.children()
.any(|n| n.kind() == SyntaxKind::COMMENT_STMT));
let col = parse("SELECT comment FROM t");
assert!(col.errors().is_empty());
assert!(
!col.syntax()
.descendants()
.any(|n| n.kind() == SyntaxKind::COMMENT_STMT),
"`comment` as a column must not parse as a COMMENT_STMT"
);
}
#[test]
fn use_role_is_one_statement_not_split() {
let p = parse("USE ROLE sysadmin");
assert!(p.errors().is_empty());
let stmts: Vec<_> = p.syntax().children().collect();
assert_eq!(stmts.len(), 1, "USE ROLE must be a single statement");
assert_eq!(stmts[0].kind(), SyntaxKind::USE_STMT);
}
#[test]
fn call_parses_into_a_call_stmt() {
let p = parse("CALL db.sch.proc(1, 2)");
assert!(p.errors().is_empty());
assert!(
p.syntax()
.children()
.any(|n| n.kind() == SyntaxKind::CALL_STMT),
"CALL should produce a CALL_STMT"
);
}
#[test]
fn expression_literals_and_bind_markers_parse_cleanly() {
let cases = [
(
"SELECT CURRENT_TIMESTAMP() - INTERVAL '1 day'",
SyntaxKind::INTERVAL_LITERAL,
),
(
"SELECT INTERVAL '1' DAY + INTERVAL 2 HOURS",
SyntaxKind::INTERVAL_LITERAL,
),
(
"SELECT [1, 2, {'nested': TRUE}] AS payload",
SyntaxKind::ARRAY_LITERAL,
),
(
"SELECT {'a': 1, 'b': [2, 3]} AS payload",
SyntaxKind::OBJECT_LITERAL,
),
(
"SELECT * FROM t WHERE id = ? AND tenant_id = :tenant_id",
SyntaxKind::BIND_MARKER,
),
(
"SELECT OBJECT_CONSTRUCT('metric_date', :v_current_date)",
SyntaxKind::BIND_MARKER,
),
];
for (sql, kind) in cases {
let parsed = parse(sql);
assert!(parsed.errors().is_empty(), "{sql}: {:?}", parsed.errors());
assert!(
parsed
.syntax()
.descendants()
.any(|node| node.kind() == kind),
"{sql} should contain {kind:?}"
);
}
let databricks = parse_with_dialect("SELECT INTERVAL 1 DAY", Dialect::Databricks);
assert!(
databricks.errors().is_empty(),
"Databricks interval literal should parse cleanly: {:?}",
databricks.errors()
);
let identifier = parse("SELECT interval FROM t");
assert!(
identifier.errors().is_empty(),
"contextual INTERVAL identifier should still parse cleanly: {:?}",
identifier.errors()
);
assert!(
!identifier
.syntax()
.descendants()
.any(|node| node.kind() == SyntaxKind::INTERVAL_LITERAL),
"bare interval identifier should not become an interval literal"
);
}
#[test]
fn grant_and_revoke_parse_into_dedicated_nodes() {
for (sql, kind) in [
("GRANT SELECT ON TABLE t TO ROLE r", SyntaxKind::GRANT_STMT),
(
"REVOKE SELECT ON TABLE t FROM ROLE r",
SyntaxKind::REVOKE_STMT,
),
] {
let p = parse(sql);
assert!(p.errors().is_empty(), "{sql} should parse cleanly");
assert!(
p.syntax().children().any(|n| n.kind() == kind),
"{sql} should produce a {kind:?}"
);
}
}
#[test]
fn create_object_kinds_without_a_body_parse_cleanly() {
for s in [
"CREATE SCHEMA IF NOT EXISTS analytics",
"CREATE OR REPLACE DATABASE d CLONE src",
"CREATE WAREHOUSE wh WITH WAREHOUSE_SIZE = XSMALL",
"CREATE SEQUENCE seq START = 1 INCREMENT = 1",
"CREATE STAGE st URL = 's3://b/p'",
"CREATE OR REPLACE FILE FORMAT ff TYPE = JSON",
] {
let p = parse(s);
assert!(p.errors().is_empty(), "{s} should parse cleanly");
assert!(
p.syntax()
.children()
.any(|n| n.kind() == SyntaxKind::CREATE_STMT),
"{s} should produce a CREATE_STMT"
);
}
}
#[test]
fn create_task_with_dml_body_is_structural() {
let src = "CREATE TASK t WAREHOUSE = wh AS\nINSERT INTO log\nSELECT 1";
let p = parse(src);
assert!(
p.errors().is_empty(),
"CREATE TASK ... AS <dml> should parse cleanly: {:?}",
p.errors()
);
assert_eq!(p.syntax().to_string(), src, "round-trip failed");
let create = p
.syntax()
.children()
.find(|n| n.kind() == SyntaxKind::CREATE_STMT)
.expect("a CREATE_STMT");
let kinds: Vec<SyntaxKind> = create.descendants().map(|n| n.kind()).collect();
assert!(
kinds.contains(&SyntaxKind::OBJECT_PROPERTY),
"expected an OBJECT_PROPERTY for WAREHOUSE = wh: {kinds:?}"
);
assert!(
kinds.contains(&SyntaxKind::INSERT_STMT),
"expected the AS body to parse as an INSERT_STMT: {kinds:?}"
);
}
#[test]
fn select_has_expected_clauses() {
let p = parse("SELECT a, b FROM t WHERE a > 1");
assert!(p.errors().is_empty());
let select = p
.syntax()
.children()
.find(|n| n.kind() == SyntaxKind::SELECT_STMT)
.expect("a SELECT_STMT");
let kinds: Vec<SyntaxKind> = select.children().map(|n| n.kind()).collect();
assert!(kinds.contains(&SyntaxKind::SELECT_LIST));
assert!(kinds.contains(&SyntaxKind::FROM_CLAUSE));
assert!(kinds.contains(&SyntaxKind::WHERE_CLAUSE));
}
#[test]
fn ast_accessors_work() {
let p = parse("SELECT a, b FROM t");
let file = SourceFile::cast(p.syntax()).expect("source file");
let select = file
.statements()
.find_map(SelectStmt::cast)
.expect("select stmt");
assert!(select.select_list().is_some());
assert_eq!(select.select_list().unwrap().items().count(), 2);
assert!(select.from_clause().is_some());
assert!(select.where_clause().is_none());
}
#[test]
fn ast_accessors_cover_more_statement_families() {
let p = parse(
"CREATE WAREHOUSE wh WITH WAREHOUSE_SIZE = 'XSMALL'; \
MERGE INTO tgt t USING src s ON t.id = s.id \
WHEN MATCHED THEN UPDATE SET t.v = s.v \
WHEN NOT MATCHED THEN INSERT (id, v) VALUES (s.id, s.v); \
BEGIN LET x := 1; RETURN x; END",
);
assert!(p.errors().is_empty(), "{:?}", p.errors());
let file = SourceFile::cast(p.syntax()).expect("source file");
let create = file
.statements_of::<CreateStmt>()
.next()
.expect("create statement");
assert!(create.child::<ObjectProperty>().is_some());
let merge = file
.statements_of::<MergeStmt>()
.next()
.expect("merge statement");
assert_eq!(merge.children::<MergeWhen>().count(), 2);
let block = file
.statements_of::<BlockStmt>()
.next()
.expect("block statement");
let stmt_list = block.child::<StmtList>().expect("block statement list");
assert!(stmt_list.child::<LetStmt>().is_some());
assert!(stmt_list.child::<ReturnStmt>().is_some());
}
#[test]
fn ast_accessors_cover_column_lists() {
let p = parse("CREATE TABLE t (a INT, b NUMBER(10, 2) DEFAULT 0)");
assert!(p.errors().is_empty(), "{:?}", p.errors());
let file = SourceFile::cast(p.syntax()).expect("source file");
let create = file
.statements_of::<CreateStmt>()
.next()
.expect("create statement");
let columns = create
.child::<ColumnDefList>()
.expect("create table column list");
assert_eq!(columns.children::<ColumnDef>().count(), 2);
}
#[test]
fn error_recovery_is_lossless_and_reported() {
let p = assert_parse_roundtrip("SELECT FROM");
assert!(!p.errors().is_empty());
}
#[test]
fn precedence_nests_correctly() {
let p = parse("SELECT a + b * c");
assert!(p.errors().is_empty(), "{:?}", p.errors());
let outer = p
.syntax()
.descendants()
.find(|n| n.kind() == SyntaxKind::BIN_EXPR)
.expect("a BIN_EXPR");
let nested = outer
.descendants()
.filter(|n| n.kind() == SyntaxKind::BIN_EXPR)
.count();
assert!(
nested >= 2,
"expected a nested BIN_EXPR for the `*` sub-expression"
);
}
#[test]
fn never_panics_on_adversarial_input() {
for s in [
";;;",
"((((",
"SELECT SELECT SELECT",
"FROM WHERE AND OR ::",
"$$ body $$ SELECT 1",
"SELECT 中文 FROM 表 WHERE x = '💥'",
"a.b.c.d.e.f.g",
"1 +",
"exists = + select$$(a(a$$ and null cluster select ",
"cluster = + select$$(a(a$$ and null cluster select ",
"[ ( $$$$ and [ $$$$ cluster ",
")",
] {
assert_parse_roundtrip(s);
}
}