mod classify;
mod columns;
mod foreign_keys;
mod location;
mod passes;
use std::fmt;
use pg_query::protobuf::node::Node;
use columns::{TableDef, collect_create_stmt};
use location::LineIndex;
use passes::{classify_pass, column_ref_pass, foreign_key_pass};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LintSeverity {
Error,
Warning,
}
impl fmt::Display for LintSeverity {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Error => f.write_str("error"),
Self::Warning => f.write_str("warning"),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LintError {
pub line: u32,
pub column: u32,
pub severity: LintSeverity,
pub message: String,
pub source: String,
}
impl fmt::Display for LintError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"{}:{}:{}: {}: {}",
self.source, self.line, self.column, self.severity, self.message
)
}
}
pub fn created_table_names(sql: &str) -> Result<Vec<String>, pg_query::Error> {
let parsed = pg_query::parse(sql)?;
Ok(parsed
.protobuf
.stmts
.iter()
.filter_map(|raw| match raw.stmt.as_ref()?.node.as_ref()? {
Node::CreateStmt(create) => collect_create_stmt(create).map(|t| t.qualified_name()),
_ => None,
})
.collect())
}
pub fn lint_declarative_schema(sql: &str, source: &str) -> Result<Vec<LintError>, Vec<LintError>> {
lint_declarative_schemas(&[(source, sql)])
}
pub fn lint_declarative_schemas(inputs: &[(&str, &str)]) -> Result<Vec<LintError>, Vec<LintError>> {
let mut errors: Vec<LintError> = Vec::new();
let mut parsed_inputs = Vec::with_capacity(inputs.len());
let mut tables: Vec<TableDef> = Vec::new();
for (source, sql) in inputs {
let parsed = match pg_query::parse(sql) {
Ok(p) => p,
Err(e) => {
errors.push(LintError {
line: 1,
column: 1,
severity: LintSeverity::Error,
message: format!("SQL parse failed: {e}"),
source: (*source).to_owned(),
});
continue;
},
};
let line_index = LineIndex::new(sql);
let (found, mut found_errors) =
classify_pass(&parsed.protobuf.stmts, sql, &line_index, source);
errors.append(&mut found_errors);
errors.extend(column_ref_pass(
&parsed.protobuf.stmts,
sql,
&line_index,
&found,
source,
));
tables.extend(found);
parsed_inputs.push((*source, *sql, parsed, line_index));
}
for (source, sql, parsed, line_index) in &parsed_inputs {
errors.extend(foreign_key_pass(
&parsed.protobuf.stmts,
sql,
line_index,
&tables,
source,
));
}
if errors.iter().any(|e| e.severity == LintSeverity::Error) {
return Err(errors);
}
Ok(errors)
}