squawk_syntax/
sql_body.rs1use 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}