1pub mod config;
2pub use config::Config;
3
4use sqlparser::ast::Statement;
5use sqlparser::dialect::{BigQueryDialect, GenericDialect, SnowflakeDialect, DuckDbDialect, PostgreSqlDialect, MySqlDialect, AnsiDialect};
6use sqlparser::parser::Parser;
7use std::path::PathBuf;
8
9pub struct Diagnostic {
11 pub rule: &'static str,
12 pub message: String,
13 pub line: usize,
15 pub col: usize,
17}
18
19pub struct FileContext {
21 pub path: PathBuf,
22 pub source: String,
23 pub statements: Vec<Statement>,
25 pub parse_errors: Vec<String>,
27}
28
29impl FileContext {
30 pub fn from_source(source: &str, path: &str) -> Self {
31 Self::from_source_with_dialect(source, path, None)
32 }
33
34 pub fn from_source_with_dialect(source: &str, path: &str, dialect: Option<&str>) -> Self {
35 let (statements, parse_errors) = match dialect {
36 Some("bigquery") => parse_with(&BigQueryDialect {}, source),
37 Some("snowflake") => parse_with(&SnowflakeDialect {}, source),
38 Some("duckdb") => parse_with(&DuckDbDialect {}, source),
39 Some("postgres") | Some("postgresql") => parse_with(&PostgreSqlDialect {}, source),
40 Some("mysql") => parse_with(&MySqlDialect {}, source),
41 Some("ansi") => parse_with(&AnsiDialect {}, source),
42 _ => parse_with(&GenericDialect {}, source),
43 };
44 FileContext {
45 path: PathBuf::from(path),
46 source: source.to_string(),
47 statements,
48 parse_errors,
49 }
50 }
51
52 pub fn lines(&self) -> impl Iterator<Item = (usize, &str)> {
54 self.source.lines().enumerate().map(|(i, line)| (i + 1, line))
55 }
56}
57
58fn parse_with<D: sqlparser::dialect::Dialect>(dialect: &D, source: &str) -> (Vec<Statement>, Vec<String>) {
59 match Parser::parse_sql(dialect, source) {
60 Ok(stmts) => (stmts, Vec::new()),
61 Err(e) => (Vec::new(), vec![e.to_string()]),
62 }
63}
64
65pub trait Rule: Send + Sync {
67 fn name(&self) -> &'static str;
68 fn check(&self, ctx: &FileContext) -> Vec<Diagnostic>;
69 fn fix(&self, _ctx: &FileContext) -> Option<String> {
71 None
72 }
73}
74
75#[cfg(test)]
76mod tests {
77 use super::*;
78
79 #[test]
80 fn valid_sql_populates_statements() {
81 let ctx = FileContext::from_source("SELECT 1; SELECT 2;", "t.sql");
82 assert_eq!(ctx.statements.len(), 2);
83 assert!(ctx.parse_errors.is_empty());
84 }
85
86 #[test]
87 fn invalid_sql_stores_parse_error() {
88 let ctx = FileContext::from_source("SELECT FROM FROM", "t.sql");
89 assert!(ctx.statements.is_empty());
90 assert!(!ctx.parse_errors.is_empty());
91 }
92
93 #[test]
94 fn empty_sql_produces_no_statements_and_no_errors() {
95 let ctx = FileContext::from_source("", "t.sql");
96 assert!(ctx.statements.is_empty());
97 assert!(ctx.parse_errors.is_empty());
98 }
99
100 #[test]
101 fn lines_still_works_after_ast_addition() {
102 let ctx = FileContext::from_source("SELECT 1\nFROM t\n", "t.sql");
103 let lines: Vec<_> = ctx.lines().collect();
104 assert_eq!(lines.len(), 2);
105 assert_eq!(lines[0], (1, "SELECT 1"));
106 assert_eq!(lines[1], (2, "FROM t"));
107 }
108}