diff --git a/crates/squawk_fmt/src/fmt.rs b/crates/squawk_fmt/src/fmt.rs index cf85da7e..704e5a8e 100644 --- a/crates/squawk_fmt/src/fmt.rs +++ b/crates/squawk_fmt/src/fmt.rs @@ -282,250 +282,219 @@ fn build_semicolon<'a>(semi: Option) -> Doc<'a> { fn build_expr<'a>(expr: ast::Expr) -> Doc<'a> { match expr { - ast::Expr::ArrayExpr(array_expr) => { - let mut doc = Doc::nil(); - - // nested parts of array expressions don't require the array token - if array_expr.array_token().is_some() { - doc = doc.append(Doc::text("array")); - }; - - if let Some(select) = array_expr.select() { - doc = doc - .append(Doc::text("(")) - .append(build_select_doc(&select)) - .append(Doc::text(")")) - } else { - doc = doc - .append(Doc::text("[")) - .append(Doc::list( - Itertools::intersperse( - array_expr.exprs().map(build_expr), - Doc::text(",").append(Doc::space()), - ) - .collect(), - )) - .append(Doc::text("]")); - } - - doc - } - ast::Expr::BetweenExpr(between_expr) => { - let mut doc = build_expr(between_expr.target().unwrap()); - if between_expr.not_token().is_some() { - doc = doc.append(Doc::space()).append(Doc::text("not")); - } - doc = doc.append(Doc::space()).append(Doc::text("between")); - match between_expr.between_symmetry() { - Some(ast::BetweenSymmetry::Asymmetric(_)) => { - doc = doc.append(Doc::space()).append(Doc::text("asymmetric")); - } - Some(ast::BetweenSymmetry::Symmetric(_)) => { - doc = doc.append(Doc::space()).append(Doc::text("symmetric")); - } - None => (), - } - doc.append(Doc::space()) - .append(build_expr(between_expr.start().unwrap())) - .append(Doc::space()) - .append(Doc::text("and")) - .append(Doc::space()) - .append(build_expr(between_expr.end().unwrap())) - } - ast::Expr::BinExpr(bin_expr) => build_expr(bin_expr.lhs().unwrap()) - .append(Doc::space()) - .append(build_op(bin_expr.op().unwrap())) - .append(Doc::space()) - .append(build_expr(bin_expr.rhs().unwrap())), + ast::Expr::ArrayExpr(array_expr) => build_array_expr(array_expr), + ast::Expr::BetweenExpr(between_expr) => build_between_expr(between_expr), + ast::Expr::BinExpr(bin_expr) => build_bin_expr(bin_expr), // ast::Expr::CallExpr(call_expr) => todo!(), // ast::Expr::CaseExpr(case_expr) => todo!(), - ast::Expr::CastExpr(cast_expr) => { - let mut doc = Doc::nil(); - if let Some(colon_colon) = cast_expr.colon_colon() { - let ty = cast_expr.ty().unwrap(); - doc = doc - .append(build_expr(cast_expr.expr().unwrap())) - .append(comments_before(colon_colon.syntax().clone())) - .append(Doc::text("::")) - .append(leading_comments(ty.syntax())) - .append(build_type(ty)) - } else if let Some(as_token) = cast_expr.as_token() { - if cast_expr.cast_token().is_some() { - doc = doc.append(Doc::text("cast")) - } else if cast_expr.treat_token().is_some() { - doc = doc.append(Doc::text("treat")) - } - let expr = cast_expr.expr().unwrap(); - let ty = cast_expr.ty().unwrap(); - if let Some(l_paren) = cast_expr.l_paren_token() { - doc = doc.append(comments_before(l_paren)); - } - doc = doc - .append(Doc::text("(")) - .append(leading_comments(expr.syntax())) - .append(build_expr(expr)) - .append(Doc::space()) - .append(leading_comments_token(&as_token)) - .append(Doc::text("as")) - .append(Doc::space()) - .append(leading_comments(ty.syntax())) - .append(build_type(ty)); - if let Some(r_paren) = cast_expr.r_paren_token() { - doc = doc.append(comments_before(r_paren)); - } - doc = doc.append(Doc::text(")")) - } else { - let literal = cast_expr.literal().unwrap(); - doc = doc - .append(build_type(cast_expr.ty().unwrap())) - .append(Doc::space()) - .append(leading_comments(literal.syntax())) - .append(build_literal(literal)); - if let Some(qualifier) = cast_expr.interval_qualifier() { - doc = doc - .append(Doc::space()) - .append(leading_comments(qualifier.syntax())) - .append(build_interval_qualifier(&qualifier)) - } - } - doc - } - ast::Expr::Collate(collate) => build_expr(collate.expr().unwrap()) - .append(Doc::space()) - .append(Doc::text("collate")) - .append(Doc::space()) - .append(Doc::text( - collate.collation_ref().unwrap().syntax().to_string(), - )), + ast::Expr::CastExpr(cast_expr) => build_cast_expr(cast_expr), + ast::Expr::Collate(collate) => build_collate_expr(collate), // ast::Expr::FieldExpr(field_expr) => todo!(), // ast::Expr::IndexExpr(index_expr) => todo!(), ast::Expr::Literal(literal) => build_literal(literal), // ast::Expr::NameRef(name_ref) => todo!(), // ast::Expr::ParenExpr(paren_expr) => todo!(), - ast::Expr::PostfixExpr(postfix_expr) => { - let expr = build_expr(postfix_expr.expr().unwrap()); - let op = match postfix_expr.op().unwrap() { - ast::PostfixOp::AtLocal(_) => Doc::text("at local"), - ast::PostfixOp::IsNull(_) => Doc::text("isnull"), - ast::PostfixOp::NotNull(_) => Doc::text("notnull"), - ast::PostfixOp::IsJson(n) => { - let mut doc = Doc::text("is json"); - if let Some(clause) = n.json_keys_unique_clause() { - doc = doc - .append(Doc::space()) - .append(build_json_keys_unique_clause(clause)); - } - doc - } - ast::PostfixOp::IsJsonArray(n) => { - let mut doc = Doc::text("is json array"); - if let Some(clause) = n.json_keys_unique_clause() { - doc = doc - .append(Doc::space()) - .append(build_json_keys_unique_clause(clause)); - } - doc - } - ast::PostfixOp::IsJsonObject(n) => { - let mut doc = Doc::text("is json object"); - if let Some(clause) = n.json_keys_unique_clause() { - doc = doc - .append(Doc::space()) - .append(build_json_keys_unique_clause(clause)); - } - doc - } - ast::PostfixOp::IsJsonScalar(n) => { - let mut doc = Doc::text("is json scalar"); - if let Some(clause) = n.json_keys_unique_clause() { - doc = doc - .append(Doc::space()) - .append(build_json_keys_unique_clause(clause)); - } - doc - } - ast::PostfixOp::IsJsonValue(n) => { - let mut doc = Doc::text("is json value"); - if let Some(clause) = n.json_keys_unique_clause() { - doc = doc - .append(Doc::space()) - .append(build_json_keys_unique_clause(clause)); - } - doc - } - ast::PostfixOp::IsNormalized(n) => { - let mut doc = Doc::text("is"); - if let Some(form) = n.unicode_normal_form() { - doc = doc - .append(Doc::space()) - .append(build_unicode_normal_form(form)); - } - doc.append(Doc::space()).append(Doc::text("normalized")) - } - ast::PostfixOp::IsNotJson(n) => { - let mut doc = Doc::text("is not json"); - if let Some(clause) = n.json_keys_unique_clause() { - doc = doc - .append(Doc::space()) - .append(build_json_keys_unique_clause(clause)); - } - doc - } - ast::PostfixOp::IsNotJsonArray(n) => { - let mut doc = Doc::text("is not json array"); - if let Some(clause) = n.json_keys_unique_clause() { - doc = doc - .append(Doc::space()) - .append(build_json_keys_unique_clause(clause)); - } - doc - } - ast::PostfixOp::IsNotJsonObject(n) => { - let mut doc = Doc::text("is not json object"); - if let Some(clause) = n.json_keys_unique_clause() { - doc = doc - .append(Doc::space()) - .append(build_json_keys_unique_clause(clause)); - } - doc - } - ast::PostfixOp::IsNotJsonScalar(n) => { - let mut doc = Doc::text("is not json scalar"); - if let Some(clause) = n.json_keys_unique_clause() { - doc = doc - .append(Doc::space()) - .append(build_json_keys_unique_clause(clause)); - } - doc - } - ast::PostfixOp::IsNotJsonValue(n) => { - let mut doc = Doc::text("is not json value"); - if let Some(clause) = n.json_keys_unique_clause() { - doc = doc - .append(Doc::space()) - .append(build_json_keys_unique_clause(clause)); - } - doc - } - ast::PostfixOp::IsNotNormalized(n) => { - let mut doc = Doc::text("is not"); - if let Some(form) = n.unicode_normal_form() { - doc = doc - .append(Doc::space()) - .append(build_unicode_normal_form(form)); - } - doc.append(Doc::space()).append(Doc::text("normalized")) - } - }; - expr.append(Doc::space()).append(op) - } - // ast::Expr::PrefixExpr(prefix_expr) => todo!(), + ast::Expr::PostfixExpr(postfix_expr) => build_postfix_expr(postfix_expr), + ast::Expr::PrefixExpr(prefix_expr) => build_prefix_expr(prefix_expr), // ast::Expr::SliceExpr(slice_expr) => todo!(), // ast::Expr::TupleExpr(tuple_expr) => todo!(), _ => Doc::text(expr.syntax().to_string()), } } +fn build_array_expr<'a>(array_expr: ast::ArrayExpr) -> Doc<'a> { + let mut doc = Doc::nil(); + + // nested parts of array expressions don't require the array token + if array_expr.array_token().is_some() { + doc = doc.append(Doc::text("array")); + }; + + if let Some(select) = array_expr.select() { + doc.append(Doc::text("(")) + .append(build_select_doc(&select)) + .append(Doc::text(")")) + } else { + doc.append(Doc::text("[")) + .append(Doc::list( + Itertools::intersperse( + array_expr.exprs().map(build_expr), + Doc::text(",").append(Doc::space()), + ) + .collect(), + )) + .append(Doc::text("]")) + } +} + +fn build_between_expr<'a>(between_expr: ast::BetweenExpr) -> Doc<'a> { + let mut doc = build_expr(between_expr.target().unwrap()); + if between_expr.not_token().is_some() { + doc = doc.append(Doc::space()).append(Doc::text("not")); + } + doc = doc.append(Doc::space()).append(Doc::text("between")); + match between_expr.between_symmetry() { + Some(ast::BetweenSymmetry::Asymmetric(_)) => { + doc = doc.append(Doc::space()).append(Doc::text("asymmetric")); + } + Some(ast::BetweenSymmetry::Symmetric(_)) => { + doc = doc.append(Doc::space()).append(Doc::text("symmetric")); + } + None => (), + } + doc.append(Doc::space()) + .append(build_expr(between_expr.start().unwrap())) + .append(Doc::space()) + .append(Doc::text("and")) + .append(Doc::space()) + .append(build_expr(between_expr.end().unwrap())) +} + +fn build_cast_expr<'a>(cast_expr: ast::CastExpr) -> Doc<'a> { + let mut doc = Doc::nil(); + if let Some(colon_colon) = cast_expr.colon_colon() { + let ty = cast_expr.ty().unwrap(); + doc = doc + .append(build_expr(cast_expr.expr().unwrap())) + .append(comments_before(colon_colon.syntax().clone())) + .append(Doc::text("::")) + .append(leading_comments(ty.syntax())) + .append(build_type(ty)) + } else if let Some(as_token) = cast_expr.as_token() { + if cast_expr.cast_token().is_some() { + doc = doc.append(Doc::text("cast")) + } else if cast_expr.treat_token().is_some() { + doc = doc.append(Doc::text("treat")) + } + let expr = cast_expr.expr().unwrap(); + let ty = cast_expr.ty().unwrap(); + if let Some(l_paren) = cast_expr.l_paren_token() { + doc = doc.append(comments_before(l_paren)); + } + doc = doc + .append(Doc::text("(")) + .append(leading_comments(expr.syntax())) + .append(build_expr(expr)) + .append(Doc::space()) + .append(leading_comments_token(&as_token)) + .append(Doc::text("as")) + .append(Doc::space()) + .append(leading_comments(ty.syntax())) + .append(build_type(ty)); + if let Some(r_paren) = cast_expr.r_paren_token() { + doc = doc.append(comments_before(r_paren)); + } + doc = doc.append(Doc::text(")")) + } else { + let literal = cast_expr.literal().unwrap(); + doc = doc + .append(build_type(cast_expr.ty().unwrap())) + .append(Doc::space()) + .append(leading_comments(literal.syntax())) + .append(build_literal(literal)); + if let Some(qualifier) = cast_expr.interval_qualifier() { + doc = doc + .append(Doc::space()) + .append(leading_comments(qualifier.syntax())) + .append(build_interval_qualifier(&qualifier)) + } + } + doc +} + +fn build_collate_expr<'a>(collate: ast::Collate) -> Doc<'a> { + build_expr(collate.expr().unwrap()) + .append(Doc::space()) + .append(Doc::text("collate")) + .append(Doc::space()) + .append(Doc::text( + collate.collation_ref().unwrap().syntax().to_string(), + )) +} + +fn build_postfix_expr<'a>(postfix_expr: ast::PostfixExpr) -> Doc<'a> { + let expr = build_expr(postfix_expr.expr().unwrap()); + let op = match postfix_expr.op().unwrap() { + ast::PostfixOp::AtLocal(_) => Doc::text("at local"), + ast::PostfixOp::IsNull(_) => Doc::text("isnull"), + ast::PostfixOp::NotNull(_) => Doc::text("notnull"), + ast::PostfixOp::IsJson(n) => build_json_postfix("is json", n.json_keys_unique_clause()), + ast::PostfixOp::IsJsonArray(n) => { + build_json_postfix("is json array", n.json_keys_unique_clause()) + } + ast::PostfixOp::IsJsonObject(n) => { + build_json_postfix("is json object", n.json_keys_unique_clause()) + } + ast::PostfixOp::IsJsonScalar(n) => { + build_json_postfix("is json scalar", n.json_keys_unique_clause()) + } + ast::PostfixOp::IsJsonValue(n) => { + build_json_postfix("is json value", n.json_keys_unique_clause()) + } + ast::PostfixOp::IsNormalized(n) => build_normalized_postfix("is", n.unicode_normal_form()), + ast::PostfixOp::IsNotJson(n) => { + build_json_postfix("is not json", n.json_keys_unique_clause()) + } + ast::PostfixOp::IsNotJsonArray(n) => { + build_json_postfix("is not json array", n.json_keys_unique_clause()) + } + ast::PostfixOp::IsNotJsonObject(n) => { + build_json_postfix("is not json object", n.json_keys_unique_clause()) + } + ast::PostfixOp::IsNotJsonScalar(n) => { + build_json_postfix("is not json scalar", n.json_keys_unique_clause()) + } + ast::PostfixOp::IsNotJsonValue(n) => { + build_json_postfix("is not json value", n.json_keys_unique_clause()) + } + ast::PostfixOp::IsNotNormalized(n) => { + build_normalized_postfix("is not", n.unicode_normal_form()) + } + }; + expr.append(Doc::space()).append(op) +} + +fn build_json_postfix<'a>( + prefix: &'static str, + clause: Option, +) -> Doc<'a> { + let mut doc = Doc::text(prefix); + if let Some(clause) = clause { + doc = doc + .append(Doc::space()) + .append(build_json_keys_unique_clause(clause)); + } + doc +} + +fn build_normalized_postfix<'a>( + prefix: &'static str, + form: Option, +) -> Doc<'a> { + let mut doc = Doc::text(prefix); + if let Some(form) = form { + doc = doc + .append(Doc::space()) + .append(build_unicode_normal_form(form)); + } + doc.append(Doc::space()).append(Doc::text("normalized")) +} + +fn build_bin_expr<'a>(bin_expr: ast::BinExpr) -> Doc<'a> { + let lhs = bin_expr.lhs().unwrap(); + let rhs = bin_expr.rhs().unwrap(); + let before_op = trailing_comments(lhs.syntax()); + let after_op = leading_comments(rhs.syntax()); + + build_expr(lhs) + .append(before_op) + .append(Doc::space()) + .append(build_op(bin_expr.op().unwrap())) + .append(Doc::space()) + .append(after_op) + .append(build_expr(rhs)) +} + fn build_json_keys_unique_clause<'a>(clause: ast::JsonKeysUniqueClause) -> Doc<'a> { let prefix = match clause { ast::JsonKeysUniqueClause::JsonWithoutUniqueKeys(_) => "without", @@ -588,7 +557,7 @@ fn build_op<'a>(op: ast::BinOp) -> Doc<'a> { ast::BinOp::Caret(_) => Doc::text("^"), ast::BinOp::ColonColon(_) => Doc::text("::"), ast::BinOp::ColonEq(_) => Doc::text(":="), - ast::BinOp::CustomOp(custom_op) => Doc::text(custom_op.syntax().to_string()), + ast::BinOp::CustomOp(custom_op) => build_operator_part(custom_op.syntax()), ast::BinOp::Eq(_) => Doc::text("="), ast::BinOp::Escape(_) => Doc::text("escape"), ast::BinOp::FatArrow(_) => Doc::text("=>"), @@ -609,7 +578,7 @@ fn build_op<'a>(op: ast::BinOp) -> Doc<'a> { ast::BinOp::NotIn(n) => build_keyword_node(n.syntax()), ast::BinOp::NotLike(n) => build_keyword_node(n.syntax()), ast::BinOp::NotSimilarTo(n) => build_keyword_node(n.syntax()), - ast::BinOp::OperatorCall(op) => Doc::text(op.syntax().to_string()), + ast::BinOp::OperatorCall(op) => build_operator_call(&op), ast::BinOp::Or(_) => Doc::text("or"), ast::BinOp::Overlaps(_) => Doc::text("overlaps"), ast::BinOp::Percent(_) => Doc::text("%"), @@ -621,6 +590,93 @@ fn build_op<'a>(op: ast::BinOp) -> Doc<'a> { } } +fn build_operator_call<'a>(operator_call: &ast::OperatorCall) -> Doc<'a> { + let mut doc = Doc::text("operator"); + + if let Some(l_paren) = operator_call.l_paren_token() { + doc = doc.append(comments_before(l_paren)); + } + doc = doc.append(Doc::text("(")); + + if let Some(op) = operator_call.op() { + doc = doc + .append(leading_comments(op.syntax())) + .append(build_operator(&op)); + } + + if let Some(r_paren) = operator_call.r_paren_token() { + doc = doc.append(comments_before(r_paren)); + } + doc.append(Doc::text(")")) +} + +fn build_prefix_expr<'a>(prefix_expr: ast::PrefixExpr) -> Doc<'a> { + let expr = prefix_expr.expr().unwrap(); + let comments = comment_tokens_before(expr.syntax().clone()); + let (op, space_before_expr) = match prefix_expr.op().unwrap() { + ast::PrefixOp::CustomOp(custom_op) => (build_operator_part(custom_op.syntax()), true), + ast::PrefixOp::Minus(_) => (Doc::text("-"), !comments.is_empty()), + ast::PrefixOp::Not(_) => (Doc::text("not"), true), + ast::PrefixOp::OperatorCall(operator_call) => (build_operator_call(&operator_call), true), + ast::PrefixOp::Plus(_) => (Doc::text("+"), !comments.is_empty()), + }; + + op.append(if space_before_expr { + Doc::space() + } else { + Doc::nil() + }) + .append(build_leading_comments(&comments)) + .append(build_expr(expr)) +} + +fn build_operator<'a>(op: &ast::Op) -> Doc<'a> { + let path_ref = op.path_ref(); + let mut doc = Doc::nil(); + + for element in op.syntax().children_with_tokens() { + match element { + rowan::NodeOrToken::Node(node) => { + doc = doc.append(match path_ref.as_ref() { + Some(path) if path.syntax() == &node => build_path_ref(path), + _ => build_operator_part(&node), + }); + } + rowan::NodeOrToken::Token(token) => { + doc = doc.append(build_operator_token(&token)); + } + } + } + + doc +} + +fn build_operator_part<'a>(node: &SyntaxNode) -> Doc<'a> { + Doc::list( + node.children_with_tokens() + .map(|element| match element { + rowan::NodeOrToken::Node(node) => build_operator_part(&node), + rowan::NodeOrToken::Token(token) => build_operator_token(&token), + }) + .collect(), + ) +} + +fn build_operator_token<'a>(token: &SyntaxToken) -> Doc<'a> { + match token.kind() { + SyntaxKind::WHITESPACE => Doc::nil(), + SyntaxKind::COMMENT => { + let doc = Doc::text(token.text().to_string()); + if is_line_comment(token) { + doc.append(Doc::hard_line()) + } else { + doc + } + } + _ => Doc::text(token.text().to_ascii_lowercase()), + } +} + fn build_literal<'a>(lit: ast::Literal) -> Doc<'a> { let Some(kind) = lit.kind() else { return Doc::nil(); diff --git a/crates/squawk_fmt/tests/after/custom_operator.snap b/crates/squawk_fmt/tests/after/custom_operator.snap new file mode 100644 index 00000000..41c9fc2b --- /dev/null +++ b/crates/squawk_fmt/tests/after/custom_operator.snap @@ -0,0 +1,6 @@ +--- +source: crates/squawk_fmt/tests/tests.rs +input_file: crates/squawk_fmt/tests/before/custom_operator.sql +--- +select 1 /*before*/ <<<< /*after*/ 1; +select ##### /*after*/ 1; diff --git a/crates/squawk_fmt/tests/after/select_expr.snap b/crates/squawk_fmt/tests/after/select_expr.snap index c59beb57..6e2c894d 100644 --- a/crates/squawk_fmt/tests/after/select_expr.snap +++ b/crates/squawk_fmt/tests/after/select_expr.snap @@ -14,6 +14,7 @@ select 2 between symmetric 1 and 3, -- bin expr 1 + 1, + 1 /* before op */ + /* after op */ 1, 2 @@@ 2, true and false, ts at time zone 'UTC', @@ -40,6 +41,7 @@ select 'foo' not similar to 'f%', 1 operator(+) 1, 1 operator(public.+) 1, + 1 operator /* before paren */(/* before path */ public /* before dot */./* before op */ + /* after op */) /* after paren */ 1, true or false, (1, 2) overlaps (3, 4), 10 % 3, @@ -88,6 +90,10 @@ select x is not nfkd normalized, -- prefix expr @-@ 10, + operator(public.+) /* after op */ 1, + +1, + -1, + not true, -- slice expr c[:2][2:], -- tuple expr diff --git a/crates/squawk_fmt/tests/before/custom_operator.sql b/crates/squawk_fmt/tests/before/custom_operator.sql new file mode 100644 index 00000000..949bd8ba --- /dev/null +++ b/crates/squawk_fmt/tests/before/custom_operator.sql @@ -0,0 +1,2 @@ +select 1 /*before*/ <<<< /*after*/ 1; +select ##### /*after*/ 1; diff --git a/crates/squawk_fmt/tests/before/select_expr.sql b/crates/squawk_fmt/tests/before/select_expr.sql index f103e5ba..68225e24 100644 --- a/crates/squawk_fmt/tests/before/select_expr.sql +++ b/crates/squawk_fmt/tests/before/select_expr.sql @@ -10,6 +10,7 @@ select 2 between symmetric 1 and 3, -- bin expr 1 + 1, + 1 /* before op */ + /* after op */ 1, 2@@@2, true and false, ts at time zone 'UTC', @@ -34,8 +35,9 @@ select 1 not in (1, 2), 'foo' not like 'f%', 'foo' not similar to 'f%', - 1 operator(+) 1, - 1 operator(public.+) 1, + 1 OPERATOR ( + ) 1, + 1 OPERATOR ( PUBLIC . + ) 1, + 1 OPERATOR /* before paren */ ( /* before path */ PUBLIC /* before dot */ . /* before op */ + /* after op */ ) /* after paren */ 1, true or false, (1, 2) overlaps (3, 4), 10 % 3, @@ -84,6 +86,10 @@ select x is not nfkd normalized, -- prefix expr @-@ 10, + OPERATOR ( PUBLIC . + ) /* after op */ 1, + + 1, + - 1, + not true, -- slice expr c[:2][2:], -- tuple expr diff --git a/crates/squawk_ide/src/goto_definition.rs b/crates/squawk_ide/src/goto_definition.rs index 40d7cf52..137287a1 100644 --- a/crates/squawk_ide/src/goto_definition.rs +++ b/crates/squawk_ide/src/goto_definition.rs @@ -12642,6 +12642,26 @@ create operator class ops for type int using btree family fa$0m as operator 1 <; "); } + #[test] + fn goto_operator_call_schema() { + assert_snapshot!(goto(" +create schema s; +create operator s.+ ( + leftarg = integer, + rightarg = integer, + function = pg_catalog.int4pl +); +select 1 operator(s$0.+) 2; +"), @" + ╭▸ + 2 │ create schema s; + │ ─ 2. destination + ‡ + 8 │ select 1 operator(s.+) 2; + ╰╴ ─ 1. source + "); + } + #[test] fn goto_create_operator_class_for_order_by_family() { assert_snapshot!(goto(" diff --git a/crates/squawk_parser/src/grammar.rs b/crates/squawk_parser/src/grammar.rs index f73ab672..e1d0d278 100644 --- a/crates/squawk_parser/src/grammar.rs +++ b/crates/squawk_parser/src/grammar.rs @@ -5052,7 +5052,7 @@ fn opt_operator(p: &mut Parser<'_>) -> bool { // > // bar.> // foo.bar.> -fn operator(p: &mut Parser<'_>) { +pub(crate) fn operator(p: &mut Parser<'_>) { let m = p.start(); opt_path_name_ref(p); if !opt_operator(p) { diff --git a/crates/squawk_parser/src/lib.rs b/crates/squawk_parser/src/lib.rs index ec2bcf52..e3b550f9 100644 --- a/crates/squawk_parser/src/lib.rs +++ b/crates/squawk_parser/src/lib.rs @@ -399,24 +399,8 @@ impl<'t> Parser<'t> { self.bump(SyntaxKind::OPERATOR_KW); self.bump(SyntaxKind::L_PAREN); - // database. - if self.eat(SyntaxKind::IDENT) { - self.expect(SyntaxKind::DOT); - } - // schema. - if self.eat(SyntaxKind::IDENT) { - self.expect(SyntaxKind::DOT); - } - - // +, -, etc. - match grammar::current_operator(self) { - Some(kind) => { - self.bump(kind); - } - None => { - self.error("expected operator"); - } - } + // e.g. `+`, `pg_catalog.+`, `db.pg_catalog.+` + grammar::operator(self); self.expect(SyntaxKind::R_PAREN); m.complete(self, SyntaxKind::OPERATOR_CALL); diff --git a/crates/squawk_parser/tests/data/ok/schemas.sql b/crates/squawk_parser/tests/data/ok/schemas.sql index 98040ae4..02fa570d 100644 --- a/crates/squawk_parser/tests/data/ok/schemas.sql +++ b/crates/squawk_parser/tests/data/ok/schemas.sql @@ -66,6 +66,9 @@ set foo to default; -- binary select 3 operator(pg_catalog.+) 4; +-- parses by PG and then reports an error about cross DB refs not allowed +select 2 operator(database_name.pg_catalog.+) 2; + select 3 operator(+) 4; select 1 operator(a.&&) 2; @@ -76,5 +79,9 @@ select operator(-) 4; select operator(a.b.-) 4; +-- keyword schema names +select 1 operator(domain.+) 2; +select 1 operator(u&"schema" uescape '!'.+) 2; + set catalog 'foo'; diff --git a/crates/squawk_parser/tests/snapshots/tests__schemas_ok.snap b/crates/squawk_parser/tests/snapshots/tests__schemas_ok.snap index 118da599..82de824d 100644 --- a/crates/squawk_parser/tests/snapshots/tests__schemas_ok.snap +++ b/crates/squawk_parser/tests/snapshots/tests__schemas_ok.snap @@ -640,15 +640,53 @@ SOURCE_FILE OPERATOR_CALL OPERATOR_KW "operator" L_PAREN "(" - IDENT "pg_catalog" - DOT "." - PLUS "+" + OP + PATH_REF + PATH_REF + PATH_SEGMENT_REF + IDENT "pg_catalog" + DOT "." + PATH_SEGMENT_REF + PLUS "+" R_PAREN ")" WHITESPACE " " LITERAL INT_NUMBER "4" SEMICOLON ";" WHITESPACE "\n\n" + COMMENT "-- parses by PG and then reports an error about cross DB refs not allowed" + WHITESPACE "\n" + SELECT + SELECT_CLAUSE + SELECT_KW "select" + WHITESPACE " " + TARGET_LIST + TARGET + BIN_EXPR + LITERAL + INT_NUMBER "2" + WHITESPACE " " + OPERATOR_CALL + OPERATOR_KW "operator" + L_PAREN "(" + OP + PATH_REF + PATH_REF + PATH_REF + PATH_SEGMENT_REF + IDENT "database_name" + DOT "." + PATH_SEGMENT_REF + IDENT "pg_catalog" + DOT "." + PATH_SEGMENT_REF + PLUS "+" + R_PAREN ")" + WHITESPACE " " + LITERAL + INT_NUMBER "2" + SEMICOLON ";" + WHITESPACE "\n\n" SELECT SELECT_CLAUSE SELECT_KW "select" @@ -662,7 +700,8 @@ SOURCE_FILE OPERATOR_CALL OPERATOR_KW "operator" L_PAREN "(" - PLUS "+" + OP + PLUS "+" R_PAREN ")" WHITESPACE " " LITERAL @@ -682,11 +721,16 @@ SOURCE_FILE OPERATOR_CALL OPERATOR_KW "operator" L_PAREN "(" - IDENT "a" - DOT "." - CUSTOM_OP - AMP "&" - AMP "&" + OP + PATH_REF + PATH_REF + PATH_SEGMENT_REF + IDENT "a" + DOT "." + PATH_SEGMENT_REF + CUSTOM_OP + AMP "&" + AMP "&" R_PAREN ")" WHITESPACE " " LITERAL @@ -705,9 +749,14 @@ SOURCE_FILE OPERATOR_CALL OPERATOR_KW "operator" L_PAREN "(" - IDENT "pg_catalog" - DOT "." - MINUS "-" + OP + PATH_REF + PATH_REF + PATH_SEGMENT_REF + IDENT "pg_catalog" + DOT "." + PATH_SEGMENT_REF + MINUS "-" R_PAREN ")" WHITESPACE " " LITERAL @@ -724,7 +773,8 @@ SOURCE_FILE OPERATOR_CALL OPERATOR_KW "operator" L_PAREN "(" - MINUS "-" + OP + MINUS "-" R_PAREN ")" WHITESPACE " " LITERAL @@ -741,16 +791,83 @@ SOURCE_FILE OPERATOR_CALL OPERATOR_KW "operator" L_PAREN "(" - IDENT "a" - DOT "." - IDENT "b" - DOT "." - MINUS "-" + OP + PATH_REF + PATH_REF + PATH_REF + PATH_SEGMENT_REF + IDENT "a" + DOT "." + PATH_SEGMENT_REF + IDENT "b" + DOT "." + PATH_SEGMENT_REF + MINUS "-" R_PAREN ")" WHITESPACE " " LITERAL INT_NUMBER "4" SEMICOLON ";" + WHITESPACE "\n\n" + COMMENT "-- keyword schema names" + WHITESPACE "\n" + SELECT + SELECT_CLAUSE + SELECT_KW "select" + WHITESPACE " " + TARGET_LIST + TARGET + BIN_EXPR + LITERAL + INT_NUMBER "1" + WHITESPACE " " + OPERATOR_CALL + OPERATOR_KW "operator" + L_PAREN "(" + OP + PATH_REF + PATH_REF + PATH_SEGMENT_REF + DOMAIN_KW "domain" + DOT "." + PATH_SEGMENT_REF + PLUS "+" + R_PAREN ")" + WHITESPACE " " + LITERAL + INT_NUMBER "2" + SEMICOLON ";" + WHITESPACE "\n" + SELECT + SELECT_CLAUSE + SELECT_KW "select" + WHITESPACE " " + TARGET_LIST + TARGET + BIN_EXPR + LITERAL + INT_NUMBER "1" + WHITESPACE " " + OPERATOR_CALL + OPERATOR_KW "operator" + L_PAREN "(" + OP + PATH_REF + PATH_REF + PATH_SEGMENT_REF + IDENT "u&\"schema\"" + WHITESPACE " " + UESCAPE_KW "uescape" + WHITESPACE " " + STRING "'!'" + DOT "." + PATH_SEGMENT_REF + PLUS "+" + R_PAREN ")" + WHITESPACE " " + LITERAL + INT_NUMBER "2" + SEMICOLON ";" WHITESPACE "\n\n\n" SET SET_KW "set" diff --git a/crates/squawk_syntax/src/ast.rs b/crates/squawk_syntax/src/ast.rs index b3caaaea..c4068066 100644 --- a/crates/squawk_syntax/src/ast.rs +++ b/crates/squawk_syntax/src/ast.rs @@ -39,7 +39,7 @@ use squawk_parser::SyntaxKind; pub use self::node_ext::normalize_name_node; pub use self::{ generated::tokens::*, - node_ext::{BinOp, LitKind, PostfixOp}, + node_ext::{BinOp, LitKind, PostfixOp, PrefixOp}, nodes::*, traits::{HasCreateTable, HasWithClause, NameLike}, }; diff --git a/crates/squawk_syntax/src/ast/generated/nodes.rs b/crates/squawk_syntax/src/ast/generated/nodes.rs index 1a399f52..dbfe0f72 100644 --- a/crates/squawk_syntax/src/ast/generated/nodes.rs +++ b/crates/squawk_syntax/src/ast/generated/nodes.rs @@ -17708,10 +17708,6 @@ impl OperatorCall { support::child(&self.syntax) } #[inline] - pub fn path_ref(&self) -> Option { - support::child(&self.syntax) - } - #[inline] pub fn l_paren_token(&self) -> Option { support::token(&self.syntax, SyntaxKind::L_PAREN) } @@ -17720,10 +17716,6 @@ impl OperatorCall { support::token(&self.syntax, SyntaxKind::R_PAREN) } #[inline] - pub fn dot_token(&self) -> Option { - support::token(&self.syntax, SyntaxKind::DOT) - } - #[inline] pub fn operator_token(&self) -> Option { support::token(&self.syntax, SyntaxKind::OPERATOR_KW) } diff --git a/crates/squawk_syntax/src/postgresql.ungram b/crates/squawk_syntax/src/postgresql.ungram index 0304582a..3f98e851 100644 --- a/crates/squawk_syntax/src/postgresql.ungram +++ b/crates/squawk_syntax/src/postgresql.ungram @@ -615,7 +615,7 @@ IsNotNormalized = 'is' UnicodeNormalForm? 'not' 'normalized' OperatorCall = - 'operator' '(' (PathRef '.')? Op ')' + 'operator' '(' Op ')' ColonEq = ':' '='