use rowan::GreenNode;
use crate::{
ast,
body::{Body, BodyLanguage},
parsing,
syntax_error::SyntaxError,
};
pub type Plpgsql = Body<ast::Plpgsql>;
impl BodyLanguage for ast::Plpgsql {
const LANGUAGE: &'static str = "plpgsql";
fn parse_text(text: &str) -> (GreenNode, Vec<SyntaxError>) {
parsing::parse_plpgsql_text(text)
}
}
impl ast::CreateFunction {
pub fn plpgsql(&self) -> Option<Plpgsql> {
Plpgsql::from_options(self.option_list()?)
}
}
impl ast::CreateProcedure {
pub fn plpgsql(&self) -> Option<Plpgsql> {
Plpgsql::from_options(self.option_list()?)
}
}
impl ast::Do {
pub fn plpgsql(&self) -> Option<Plpgsql> {
if let Some(language) = self.do_language()
&& !ast::Plpgsql::is_language(language.language_name())
{
return None;
}
Some(Plpgsql::parse(self.body()?.decoded_value()?))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::SourceFile;
use crate::ast::AstNode;
use crate::test::render_errors;
use insta::assert_snapshot;
use rowan::{TextRange, TextSize};
fn find(sql: &str) -> Option<Plpgsql> {
let parse = SourceFile::parse(sql);
assert!(parse.errors().is_empty(), "{:?}", parse.errors());
parse.tree().syntax().descendants().find_map(|node| {
ast::CreateFunction::cast(node.clone())
.and_then(|it| it.plpgsql())
.or_else(|| ast::CreateProcedure::cast(node.clone()).and_then(|it| it.plpgsql()))
.or_else(|| ast::Do::cast(node.clone()).and_then(|it| it.plpgsql()))
})
}
fn body(sql: &str) -> String {
let body = find(sql).expect("no plpgsql body");
let range = body.source_range(TextRange::up_to(TextSize::of(body.text())));
let start = usize::from(range.start());
let end = usize::from(range.end());
let mut out = format!("{:#?}", body.syntax());
out.push_str(&format!("---\nsource {range:?} {:?}\n", &sql[start..end]));
out.push_str(&render_errors(sql, &body.errors()));
out
}
#[test]
fn language_after_as() {
assert_snapshot!(
body("create function f() returns int as $$ begin null; end $$ language plpgsql;"),
@r#"
PLPGSQL@0..17
WHITESPACE@0..1 " "
PLPGSQL_BLOCK@1..16
BEGIN_KW@1..6 "begin"
WHITESPACE@6..7 " "
PLPGSQL_BODY@7..12
PLPGSQL_NULL_STMT@7..12
NULL_KW@7..11 "null"
SEMICOLON@11..12 ";"
WHITESPACE@12..13 " "
END_KW@13..16 "end"
WHITESPACE@16..17 " "
---
source 37..54 " begin null; end "
"#
);
}
#[test]
fn language_before_as() {
assert_snapshot!(
body("create function f() returns int language plpgsql as $$ begin null; end $$;"),
@r#"
PLPGSQL@0..17
WHITESPACE@0..1 " "
PLPGSQL_BLOCK@1..16
BEGIN_KW@1..6 "begin"
WHITESPACE@6..7 " "
PLPGSQL_BODY@7..12
PLPGSQL_NULL_STMT@7..12
NULL_KW@7..11 "null"
SEMICOLON@11..12 ";"
WHITESPACE@12..13 " "
END_KW@13..16 "end"
WHITESPACE@16..17 " "
---
source 54..71 " begin null; end "
"#
);
}
#[test]
fn other_language_is_not_a_body() {
assert!(find("create function f() returns int as $$ select 1 $$ language sql;").is_none());
}
#[test]
fn procedure() {
assert_snapshot!(
body("create procedure p() as $$ begin null; end $$ language plpgsql;"),
@r#"
PLPGSQL@0..17
WHITESPACE@0..1 " "
PLPGSQL_BLOCK@1..16
BEGIN_KW@1..6 "begin"
WHITESPACE@6..7 " "
PLPGSQL_BODY@7..12
PLPGSQL_NULL_STMT@7..12
NULL_KW@7..11 "null"
SEMICOLON@11..12 ";"
WHITESPACE@12..13 " "
END_KW@13..16 "end"
WHITESPACE@16..17 " "
---
source 26..43 " begin null; end "
"#
);
}
#[test]
fn do_defaults_to_plpgsql() {
assert_snapshot!(body("do $$ begin null; end $$;"), @r#"
PLPGSQL@0..17
WHITESPACE@0..1 " "
PLPGSQL_BLOCK@1..16
BEGIN_KW@1..6 "begin"
WHITESPACE@6..7 " "
PLPGSQL_BODY@7..12
PLPGSQL_NULL_STMT@7..12
NULL_KW@7..11 "null"
SEMICOLON@11..12 ";"
WHITESPACE@12..13 " "
END_KW@13..16 "end"
WHITESPACE@16..17 " "
---
source 5..22 " begin null; end "
"#
);
}
#[test]
fn do_with_other_language_is_not_a_body() {
assert!(find("do language plpython3u $$ return 1 $$;").is_none());
}
#[test]
fn escaped_body_maps_back_through_the_escapes() {
assert_snapshot!(
body(r"create function f() returns int as E'begin null; end\n' language plpgsql;"),
@r#"
PLPGSQL@0..16
PLPGSQL_BLOCK@0..15
BEGIN_KW@0..5 "begin"
WHITESPACE@5..6 " "
PLPGSQL_BODY@6..11
PLPGSQL_NULL_STMT@6..11
NULL_KW@6..10 "null"
SEMICOLON@10..11 ";"
WHITESPACE@11..12 " "
END_KW@12..15 "end"
WHITESPACE@15..16 "\n"
---
source 37..54 "begin null; end\\n"
"#
);
}
#[test]
fn unparsed_tokens_are_errors() {
assert_snapshot!(body("do $$ begin null null; end $$;"), @r#"
PLPGSQL@0..22
WHITESPACE@0..1 " "
PLPGSQL_BLOCK@1..21
BEGIN_KW@1..6 "begin"
WHITESPACE@6..7 " "
PLPGSQL_BODY@7..17
ERROR@7..17
NULL_KW@7..11 "null"
WHITESPACE@11..12 " "
NULL_KW@12..16 "null"
SEMICOLON@16..17 ";"
WHITESPACE@17..18 " "
END_KW@18..21 "end"
WHITESPACE@21..22 " "
---
source 5..27 " begin null null; end "
error[syntax-error]: expected a statement, got NULL_KW
╭▸
1 │ do $$ begin null null; end $$;
╰╴ ━
"#);
}
#[test]
fn errors_in_an_escaped_body_map_into_the_file() {
assert_snapshot!(body(r"do E'begin\n null null;\n end';"), @r#"
PLPGSQL@0..22
PLPGSQL_BLOCK@0..22
BEGIN_KW@0..5 "begin"
WHITESPACE@5..7 "\n "
PLPGSQL_BODY@7..17
ERROR@7..17
NULL_KW@7..11 "null"
WHITESPACE@11..12 " "
NULL_KW@12..16 "null"
SEMICOLON@16..17 ";"
WHITESPACE@17..19 "\n "
END_KW@19..22 "end"
---
source 5..29 "begin\\n null null;\\n end"
error[syntax-error]: expected a statement, got NULL_KW
╭▸
1 │ do E'begin\n null null;\n end';
╰╴ ━
"#);
}
}