squawk-syntax 2.66.0

Linter for Postgres migrations & SQL
Documentation
use rowan::GreenNode;

use crate::{
    ast,
    ast::AstNode,
    body::{Body, BodyLanguage},
    parsing,
    syntax_error::SyntaxError,
};

pub type SqlBody = Body<ast::SourceFile>;

impl BodyLanguage for ast::SourceFile {
    const LANGUAGE: &'static str = "sql";

    fn parse_text(text: &str) -> (GreenNode, Vec<SyntaxError>) {
        parsing::parse_text(text)
    }
}

impl ast::SourceFile {
    pub fn sql_body_errors(&self) -> Vec<SyntaxError> {
        self.syntax()
            .descendants()
            .filter_map(|node| {
                ast::CreateFunction::cast(node.clone())
                    .and_then(|function| function.sql_body())
                    .or_else(|| {
                        ast::CreateProcedure::cast(node).and_then(|procedure| procedure.sql_body())
                    })
            })
            .flat_map(|body| body.errors())
            .collect()
    }
}

impl ast::CreateFunction {
    pub fn sql_body(&self) -> Option<SqlBody> {
        SqlBody::from_options(self.option_list()?)
    }
}

impl ast::CreateProcedure {
    pub fn sql_body(&self) -> Option<SqlBody> {
        SqlBody::from_options(self.option_list()?)
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::SourceFile;
    use crate::test::render_errors;
    use insta::assert_snapshot;
    use rowan::{TextRange, TextSize};

    fn find(sql: &str) -> Option<SqlBody> {
        let parse = SourceFile::parse(sql);

        parse.tree().syntax().descendants().find_map(|node| {
            ast::CreateFunction::cast(node.clone())
                .and_then(|it| it.sql_body())
                .or_else(|| ast::CreateProcedure::cast(node).and_then(|it| it.sql_body()))
        })
    }

    fn body(sql: &str) -> String {
        let body = find(sql).expect("no SQL 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 function_language_before_as() {
        assert_snapshot!(body(
            "\
create function f(int) returns int
  language sql
  as $$ select $1 + 1 $$;"
        ));
    }

    #[test]
    fn function_language_after_as() {
        assert_snapshot!(body(
            "\
create function f(int) returns int
  as 'select $1'
  language sql;"
        ));
    }

    #[test]
    fn procedure() {
        assert_snapshot!(body(
            "\
create procedure p()
  language sql
  as $$ select 1; select 2 $$;"
        ));
    }

    #[test]
    fn other_language_is_not_sql() {
        assert!(
            find(
                "\
create function f() returns int
  as $$ select 1 $$
  language plpgsql;"
            )
            .is_none()
        );
    }

    #[test]
    fn string_language_is_case_sensitive() {
        assert!(
            find(
                "\
create function f() returns int
  as 'not even sql'
  language 'SQL';"
            )
            .is_none()
        );
    }

    #[test]
    fn inline_body_is_not_a_string_body() {
        assert!(
            find(
                "\
create function f() returns int
  language sql
  return 1;"
            )
            .is_none()
        );
    }

    #[test]
    fn errors_map_into_the_containing_file() {
        assert_snapshot!(body(
            "\
create function f() returns int
  language sql
  as $$ select from $$;"
        ));
    }
}