pub mod config;
pub use config::Config;
use sqlparser::ast::Statement;
use sqlparser::dialect::{BigQueryDialect, GenericDialect, SnowflakeDialect, DuckDbDialect, PostgreSqlDialect, MySqlDialect, AnsiDialect};
use sqlparser::parser::Parser;
use std::path::PathBuf;
pub struct Diagnostic {
pub rule: &'static str,
pub message: String,
pub line: usize,
pub col: usize,
}
pub struct FileContext {
pub path: PathBuf,
pub source: String,
pub statements: Vec<Statement>,
pub parse_errors: Vec<String>,
}
impl FileContext {
pub fn from_source(source: &str, path: &str) -> Self {
Self::from_source_with_dialect(source, path, None)
}
pub fn from_source_with_dialect(source: &str, path: &str, dialect: Option<&str>) -> Self {
let (statements, parse_errors) = match dialect {
Some("bigquery") => parse_with(&BigQueryDialect {}, source),
Some("snowflake") => parse_with(&SnowflakeDialect {}, source),
Some("duckdb") => parse_with(&DuckDbDialect {}, source),
Some("postgres") | Some("postgresql") => parse_with(&PostgreSqlDialect {}, source),
Some("mysql") => parse_with(&MySqlDialect {}, source),
Some("ansi") => parse_with(&AnsiDialect {}, source),
_ => parse_with(&GenericDialect {}, source),
};
FileContext {
path: PathBuf::from(path),
source: source.to_string(),
statements,
parse_errors,
}
}
pub fn lines(&self) -> impl Iterator<Item = (usize, &str)> {
self.source.lines().enumerate().map(|(i, line)| (i + 1, line))
}
}
fn parse_with<D: sqlparser::dialect::Dialect>(dialect: &D, source: &str) -> (Vec<Statement>, Vec<String>) {
match Parser::parse_sql(dialect, source) {
Ok(stmts) => (stmts, Vec::new()),
Err(e) => (Vec::new(), vec![e.to_string()]),
}
}
pub trait Rule: Send + Sync {
fn name(&self) -> &'static str;
fn check(&self, ctx: &FileContext) -> Vec<Diagnostic>;
fn fix(&self, _ctx: &FileContext) -> Option<String> {
None
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn valid_sql_populates_statements() {
let ctx = FileContext::from_source("SELECT 1; SELECT 2;", "t.sql");
assert_eq!(ctx.statements.len(), 2);
assert!(ctx.parse_errors.is_empty());
}
#[test]
fn invalid_sql_stores_parse_error() {
let ctx = FileContext::from_source("SELECT FROM FROM", "t.sql");
assert!(ctx.statements.is_empty());
assert!(!ctx.parse_errors.is_empty());
}
#[test]
fn empty_sql_produces_no_statements_and_no_errors() {
let ctx = FileContext::from_source("", "t.sql");
assert!(ctx.statements.is_empty());
assert!(ctx.parse_errors.is_empty());
}
#[test]
fn lines_still_works_after_ast_addition() {
let ctx = FileContext::from_source("SELECT 1\nFROM t\n", "t.sql");
let lines: Vec<_> = ctx.lines().collect();
assert_eq!(lines.len(), 2);
assert_eq!(lines[0], (1, "SELECT 1"));
assert_eq!(lines[1], (2, "FROM t"));
}
}