diff --git a/crates/squawk_fmt/tests/after/create_schema.snap b/crates/squawk_fmt/tests/after/create_schema.snap index 25af8cb7..15d2df0d 100644 --- a/crates/squawk_fmt/tests/after/create_schema.snap +++ b/crates/squawk_fmt/tests/after/create_schema.snap @@ -4,6 +4,8 @@ input_file: crates/squawk_fmt/tests/before/create_schema.sql --- create schema app; +create schema تست; + create schema if not exists extraordinarily_long_application_reporting_and_data_warehouse_schema authorization extraordinarily_long_application_reporting_and_data_warehouse_owner_role; diff --git a/crates/squawk_fmt/tests/before/create_schema.sql b/crates/squawk_fmt/tests/before/create_schema.sql index fb68c20e..dbe6b354 100644 --- a/crates/squawk_fmt/tests/before/create_schema.sql +++ b/crates/squawk_fmt/tests/before/create_schema.sql @@ -1,5 +1,7 @@ create schema app; +create schema تست; + create schema if not exists extraordinarily_long_application_reporting_and_data_warehouse_schema authorization extraordinarily_long_application_reporting_and_data_warehouse_owner_role; create schema authorization current_user; diff --git a/crates/squawk_parser/src/grammar.rs b/crates/squawk_parser/src/grammar.rs index 3e94a3c9..8ab75478 100644 --- a/crates/squawk_parser/src/grammar.rs +++ b/crates/squawk_parser/src/grammar.rs @@ -3400,6 +3400,10 @@ fn with_query_clause(p: &mut Parser<'_>) -> Option { let m = p.start(); p.expect(WITH_KW); p.eat(RECURSIVE_KW); + if p.at_ts(WITH_FOLLOW) { + p.error("expected common table expression"); + return Some(m.complete(p, WITH_CLAUSE)); + } while !p.at(EOF) { with_query(p); if p.at(COMMA) && p.nth_at_ts(1, WITH_FOLLOW) { @@ -3409,8 +3413,11 @@ fn with_query_clause(p: &mut Parser<'_>) -> Option { if !p.eat(COMMA) { if p.at_ts(WITH_FOLLOW) || (p.at(L_PAREN) && p.nth_at_ts(1, PAREN_SELECT_FIRST)) { break; - } else { + } else if p.at_ts(NAME_REF_FIRST) { p.error("missing comma"); + } else { + p.err_and_bump("missing comma"); + break; } } } diff --git a/crates/squawk_parser/tests/data/err/select_cte.sql b/crates/squawk_parser/tests/data/err/select_cte.sql index 64ec5ba0..12241fd0 100644 --- a/crates/squawk_parser/tests/data/err/select_cte.sql +++ b/crates/squawk_parser/tests/data/err/select_cte.sql @@ -45,3 +45,6 @@ values (2); -- WITHOUT OVERLAPS is only valid w/ PRIMARY KEY or UNIQUE constraint with t (a without overlaps) as (select 1) select * from t; + +-- missing common table expression +with select 1; diff --git a/crates/squawk_parser/tests/snapshots/tests__select_cte_err.snap b/crates/squawk_parser/tests/snapshots/tests__select_cte_err.snap index 1d8adb96..d73c66c8 100644 --- a/crates/squawk_parser/tests/snapshots/tests__select_cte_err.snap +++ b/crates/squawk_parser/tests/snapshots/tests__select_cte_err.snap @@ -457,6 +457,21 @@ SOURCE_FILE PATH_SEGMENT_REF IDENT "t" SEMICOLON ";" + WHITESPACE "\n\n" + COMMENT "-- missing common table expression" + WHITESPACE "\n" + SELECT + WITH_CLAUSE + WITH_KW "with" + WHITESPACE " " + SELECT_CLAUSE + SELECT_KW "select" + WHITESPACE " " + TARGET_LIST + TARGET + LITERAL + INT_NUMBER "1" + SEMICOLON ";" WHITESPACE "\n" --- error[syntax-error]: unexpected comma @@ -491,3 +506,7 @@ error[syntax-error]: WITHOUT OVERLAPS is only allowed in PRIMARY KEY and UNIQUE ╭▸ 47 │ with t (a without overlaps) as (select 1) select * from t; ╰╴ ━ +error[syntax-error]: expected common table expression + ╭▸ +50 │ with select 1; + ╰╴ ━ diff --git a/crates/squawk_syntax/src/ast/node_ext.rs b/crates/squawk_syntax/src/ast/node_ext.rs index 307ed25f..3fb4bd40 100644 --- a/crates/squawk_syntax/src/ast/node_ext.rs +++ b/crates/squawk_syntax/src/ast/node_ext.rs @@ -1000,9 +1000,21 @@ impl ast::PathSegmentRef { } pub fn is_quoted_name_node(node: &SyntaxNode) -> bool { - let text = node.text(); - let first = text.char_at(0.into()); - let second = text.char_at(1.into()); + let mut first = None; + let mut second = None; + node.text().for_each_chunk(|chunk| { + if second.is_some() { + return; + } + for ch in chunk.chars() { + if first.is_none() { + first = Some(ch); + } else { + second = Some(ch); + break; + } + } + }); matches!( (first, second), (Some('u' | 'U'), Some('"')) | (Some('"'), Some(_)) diff --git a/crates/squawk_syntax/src/quote.rs b/crates/squawk_syntax/src/quote.rs index e27d585e..feb0aab6 100644 --- a/crates/squawk_syntax/src/quote.rs +++ b/crates/squawk_syntax/src/quote.rs @@ -64,12 +64,12 @@ pub fn unquote_ident(node: &SyntaxNode) -> Option { // see: https://www.postgresql.org/docs/18/sql-syntax-lexical.html#SQL-SYNTAX-IDENTIFIERS match chars.next() { - Some(c) if c.is_lowercase() || c == '_' => {} + Some(c) if c.is_ascii_lowercase() || !c.is_ascii() || c == '_' => {} _ => return None, } for c in chars { - if c.is_lowercase() || c.is_ascii_digit() || c == '_' || c == '$' { + if c.is_ascii_lowercase() || !c.is_ascii() || c.is_ascii_digit() || c == '_' || c == '$' { continue; } return None; @@ -90,12 +90,12 @@ pub fn needs_quoting(text: &str) -> bool { let mut chars = text.chars(); match chars.next() { - Some(c) if c.is_lowercase() || c == '_' => {} + Some(c) if c.is_ascii_lowercase() || !c.is_ascii() || c == '_' => {} _ => return true, } for c in chars { - if c.is_lowercase() || c.is_ascii_digit() || c == '_' || c == '$' { + if c.is_ascii_lowercase() || !c.is_ascii() || c.is_ascii_digit() || c == '_' || c == '$' { continue; } return true; @@ -205,6 +205,7 @@ mod tests { assert_snapshot!(quote_ident("col_name"), @"col_name"); assert_snapshot!(quote_ident("users"), @"users"); assert_snapshot!(quote_ident("t2$"), @"t2$"); + assert_snapshot!(quote_ident("تست"), @"تست"); } #[test]