Skip to main content

squawk_syntax/
sql_body.rs

1use rowan::GreenNode;
2
3use crate::{
4    ast,
5    ast::AstNode,
6    body::{Body, BodyLanguage},
7    parsing,
8    syntax_error::SyntaxError,
9};
10
11pub type SqlBody = Body<ast::SourceFile>;
12
13impl BodyLanguage for ast::SourceFile {
14    const LANGUAGE: &'static str = "sql";
15
16    fn parse_text(text: &str) -> (GreenNode, Vec<SyntaxError>) {
17        parsing::parse_text(text)
18    }
19}
20
21impl ast::SourceFile {
22    pub fn sql_body_errors(&self) -> Vec<SyntaxError> {
23        self.syntax()
24            .descendants()
25            .filter_map(|node| {
26                ast::CreateFunction::cast(node.clone())
27                    .and_then(|function| function.sql_body())
28                    .or_else(|| {
29                        ast::CreateProcedure::cast(node).and_then(|procedure| procedure.sql_body())
30                    })
31            })
32            .flat_map(|body| body.errors())
33            .collect()
34    }
35}
36
37impl ast::CreateFunction {
38    pub fn sql_body(&self) -> Option<SqlBody> {
39        SqlBody::from_options(self.option_list()?)
40    }
41}
42
43impl ast::CreateProcedure {
44    pub fn sql_body(&self) -> Option<SqlBody> {
45        SqlBody::from_options(self.option_list()?)
46    }
47}
48
49#[cfg(test)]
50mod tests {
51    use super::*;
52    use crate::SourceFile;
53    use crate::test::render_errors;
54    use insta::assert_snapshot;
55    use rowan::{TextRange, TextSize};
56
57    fn find(sql: &str) -> Option<SqlBody> {
58        let parse = SourceFile::parse(sql);
59
60        parse.tree().syntax().descendants().find_map(|node| {
61            ast::CreateFunction::cast(node.clone())
62                .and_then(|it| it.sql_body())
63                .or_else(|| ast::CreateProcedure::cast(node).and_then(|it| it.sql_body()))
64        })
65    }
66
67    fn body(sql: &str) -> String {
68        let body = find(sql).expect("no SQL body");
69
70        let range = body.source_range(TextRange::up_to(TextSize::of(body.text())));
71        let start = usize::from(range.start());
72        let end = usize::from(range.end());
73
74        let mut out = format!("{:#?}", body.syntax());
75        out.push_str(&format!("---\nsource {range:?} {:?}\n", &sql[start..end]));
76        out.push_str(&render_errors(sql, &body.errors()));
77        out
78    }
79
80    #[test]
81    fn function_language_before_as() {
82        assert_snapshot!(body(
83            "\
84create function f(int) returns int
85  language sql
86  as $$ select $1 + 1 $$;"
87        ));
88    }
89
90    #[test]
91    fn function_language_after_as() {
92        assert_snapshot!(body(
93            "\
94create function f(int) returns int
95  as 'select $1'
96  language sql;"
97        ));
98    }
99
100    #[test]
101    fn procedure() {
102        assert_snapshot!(body(
103            "\
104create procedure p()
105  language sql
106  as $$ select 1; select 2 $$;"
107        ));
108    }
109
110    #[test]
111    fn other_language_is_not_sql() {
112        assert!(
113            find(
114                "\
115create function f() returns int
116  as $$ select 1 $$
117  language plpgsql;"
118            )
119            .is_none()
120        );
121    }
122
123    #[test]
124    fn string_language_is_case_sensitive() {
125        assert!(
126            find(
127                "\
128create function f() returns int
129  as 'not even sql'
130  language 'SQL';"
131            )
132            .is_none()
133        );
134    }
135
136    #[test]
137    fn inline_body_is_not_a_string_body() {
138        assert!(
139            find(
140                "\
141create function f() returns int
142  language sql
143  return 1;"
144            )
145            .is_none()
146        );
147    }
148
149    #[test]
150    fn errors_map_into_the_containing_file() {
151        assert_snapshot!(body(
152            "\
153create function f() returns int
154  language sql
155  as $$ select from $$;"
156        ));
157    }
158}