use crate::linter::config::LintConfig;
use crate::linter::rule::{LintContext, LintRule};
use crate::types::{issue_codes, Dialect, Issue, IssueAutofixApplicability, IssuePatchEdit, Span};
use sqlparser::ast::{
CreateView, Expr, FromTable, Ident, Merge, Query, SetExpr, Statement, TableFactor,
TableWithJoins, Update, UpdateTableFromKind,
};
use sqlparser::tokenizer::{Token, TokenWithSpan, Tokenizer, Whitespace};
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum AliasingPreference {
Explicit,
Implicit,
}
impl AliasingPreference {
fn from_config(config: &LintConfig, rule_code: &str) -> Self {
match config
.rule_option_str(rule_code, "aliasing")
.unwrap_or("explicit")
.to_ascii_lowercase()
.as_str()
{
"implicit" => Self::Implicit,
_ => Self::Explicit,
}
}
fn message(self) -> &'static str {
match self {
Self::Explicit => "Use explicit AS when aliasing tables.",
Self::Implicit => "Use implicit aliasing when aliasing tables (omit AS).",
}
}
fn violation(self, explicit_as: bool) -> bool {
match self {
Self::Explicit => !explicit_as,
Self::Implicit => explicit_as,
}
}
}
pub struct AliasingTableStyle {
aliasing: AliasingPreference,
}
impl AliasingTableStyle {
pub fn from_config(config: &LintConfig) -> Self {
Self {
aliasing: AliasingPreference::from_config(config, issue_codes::LINT_AL_001),
}
}
}
impl Default for AliasingTableStyle {
fn default() -> Self {
Self {
aliasing: AliasingPreference::Explicit,
}
}
}
impl LintRule for AliasingTableStyle {
fn code(&self) -> &'static str {
issue_codes::LINT_AL_001
}
fn name(&self) -> &'static str {
"Table alias style"
}
fn description(&self) -> &'static str {
"Implicit/explicit aliasing of table."
}
fn check(&self, statement: &Statement, ctx: &LintContext) -> Vec<Issue> {
let mut issues = Vec::new();
let tokens =
tokenized_for_context(ctx).or_else(|| tokenized(ctx.statement_sql(), ctx.dialect()));
collect_table_aliases_in_statement(statement, &mut |alias| {
let Some(occurrence) = alias_occurrence_in_statement(alias, ctx, tokens.as_deref())
else {
return;
};
if !self.aliasing.violation(occurrence.explicit_as) {
return;
}
let mut issue = Issue::warning(issue_codes::LINT_AL_001, self.aliasing.message())
.with_statement(ctx.statement_index)
.with_span(ctx.span_from_statement_offset(occurrence.start, occurrence.end));
if let Some(edits) = autofix_edits_for_occurrence(occurrence, self.aliasing) {
issue = issue.with_autofix_edits(IssueAutofixApplicability::Safe, edits);
}
issues.push(issue);
});
issues
}
}
#[derive(Clone, Copy)]
struct AliasOccurrence {
start: usize,
end: usize,
explicit_as: bool,
as_span: Option<Span>,
has_leading_whitespace: bool,
}
fn autofix_edits_for_occurrence(
occurrence: AliasOccurrence,
aliasing: AliasingPreference,
) -> Option<Vec<IssuePatchEdit>> {
match aliasing {
AliasingPreference::Explicit if !occurrence.explicit_as => {
let insert = Span::new(occurrence.start, occurrence.start);
let replacement = if occurrence.has_leading_whitespace {
"AS "
} else {
" AS "
};
Some(vec![IssuePatchEdit::new(insert, replacement)])
}
AliasingPreference::Implicit if occurrence.explicit_as => {
let as_span = occurrence.as_span?;
let delete_end = occurrence.start;
Some(vec![IssuePatchEdit::new(
Span::new(as_span.start, delete_end),
" ",
)])
}
_ => None,
}
}
fn alias_occurrence_in_statement(
alias: &Ident,
ctx: &LintContext,
tokens: Option<&[LocatedToken]>,
) -> Option<AliasOccurrence> {
let tokens = tokens?;
let abs_start = line_col_to_offset(
ctx.sql,
alias.span.start.line as usize,
alias.span.start.column as usize,
)?;
let abs_end = line_col_to_offset(
ctx.sql,
alias.span.end.line as usize,
alias.span.end.column as usize,
)?;
if abs_start < ctx.statement_range.start || abs_end > ctx.statement_range.end {
return None;
}
let rel_start = abs_start - ctx.statement_range.start;
let rel_end = abs_end - ctx.statement_range.start;
let (explicit_as, as_span) = explicit_as_before_alias_tokens(tokens, rel_start)?;
let has_leading_whitespace = has_whitespace_before(tokens, rel_start);
Some(AliasOccurrence {
start: rel_start,
end: rel_end,
explicit_as,
as_span,
has_leading_whitespace,
})
}
fn collect_table_aliases_in_statement<F: FnMut(&Ident)>(statement: &Statement, visitor: &mut F) {
match statement {
Statement::Query(query) => collect_table_aliases_in_query(query, visitor),
Statement::Insert(insert) => {
if let Some(source) = &insert.source {
collect_table_aliases_in_query(source, visitor);
}
}
Statement::CreateView(CreateView { query, .. }) => {
collect_table_aliases_in_query(query, visitor)
}
Statement::CreateTable(create) => {
if let Some(query) = &create.query {
collect_table_aliases_in_query(query, visitor);
}
}
Statement::Update(Update {
table,
from,
selection,
..
}) => {
for join in &table.joins {
collect_table_aliases_in_table_factor(&join.relation, visitor);
}
if let Some(from) = from {
match from {
UpdateTableFromKind::BeforeSet(tables)
| UpdateTableFromKind::AfterSet(tables) => {
for t in tables {
collect_table_aliases_in_table_with_joins(t, visitor);
}
}
}
}
if let Some(selection) = selection {
collect_table_aliases_in_expr(selection, visitor);
}
}
Statement::Delete(delete) => {
match &delete.from {
FromTable::WithFromKeyword(tables) | FromTable::WithoutKeyword(tables) => {
for t in tables {
collect_table_aliases_in_table_with_joins(t, visitor);
}
}
}
if let Some(using) = &delete.using {
for t in using {
collect_table_aliases_in_table_with_joins(t, visitor);
}
}
if let Some(selection) = &delete.selection {
collect_table_aliases_in_expr(selection, visitor);
}
}
Statement::Merge(Merge { table, source, .. }) => {
collect_table_aliases_in_table_factor(table, visitor);
collect_table_aliases_in_table_factor(source, visitor);
}
_ => {}
}
}
fn collect_table_aliases_in_query<F: FnMut(&Ident)>(query: &Query, visitor: &mut F) {
if let Some(with) = &query.with {
for cte in &with.cte_tables {
collect_table_aliases_in_query(&cte.query, visitor);
}
}
collect_table_aliases_in_set_expr(&query.body, visitor);
}
fn collect_table_aliases_in_set_expr<F: FnMut(&Ident)>(set_expr: &SetExpr, visitor: &mut F) {
match set_expr {
SetExpr::Select(select) => {
for table in &select.from {
collect_table_aliases_in_table_with_joins(table, visitor);
for join in &table.joins {
if let Some(expr) = join_constraint_expr(&join.join_operator) {
collect_table_aliases_in_expr(expr, visitor);
}
}
}
if let Some(selection) = &select.selection {
collect_table_aliases_in_expr(selection, visitor);
}
if let Some(having) = &select.having {
collect_table_aliases_in_expr(having, visitor);
}
if let Some(qualify) = &select.qualify {
collect_table_aliases_in_expr(qualify, visitor);
}
for item in &select.projection {
match item {
sqlparser::ast::SelectItem::UnnamedExpr(expr)
| sqlparser::ast::SelectItem::ExprWithAlias { expr, .. } => {
collect_table_aliases_in_expr(expr, visitor);
}
_ => {}
}
}
}
SetExpr::Query(query) => collect_table_aliases_in_query(query, visitor),
SetExpr::SetOperation { left, right, .. } => {
collect_table_aliases_in_set_expr(left, visitor);
collect_table_aliases_in_set_expr(right, visitor);
}
SetExpr::Insert(statement)
| SetExpr::Update(statement)
| SetExpr::Delete(statement)
| SetExpr::Merge(statement) => collect_table_aliases_in_statement(statement, visitor),
_ => {}
}
}
fn collect_table_aliases_in_table_with_joins<F: FnMut(&Ident)>(
table_with_joins: &TableWithJoins,
visitor: &mut F,
) {
collect_table_aliases_in_table_factor(&table_with_joins.relation, visitor);
for join in &table_with_joins.joins {
collect_table_aliases_in_table_factor(&join.relation, visitor);
}
}
fn collect_table_aliases_in_table_factor<F: FnMut(&Ident)>(
table_factor: &TableFactor,
visitor: &mut F,
) {
if let Some(alias) = table_factor_alias_ident(table_factor) {
visitor(alias);
}
match table_factor {
TableFactor::Derived { subquery, .. } => collect_table_aliases_in_query(subquery, visitor),
TableFactor::NestedJoin {
table_with_joins, ..
} => collect_table_aliases_in_table_with_joins(table_with_joins, visitor),
TableFactor::Pivot { table, .. }
| TableFactor::Unpivot { table, .. }
| TableFactor::MatchRecognize { table, .. } => {
collect_table_aliases_in_table_factor(table, visitor)
}
_ => {}
}
}
fn collect_table_aliases_in_expr<F: FnMut(&Ident)>(expr: &Expr, visitor: &mut F) {
match expr {
Expr::Subquery(query)
| Expr::Exists {
subquery: query, ..
} => {
collect_table_aliases_in_query(query, visitor);
}
Expr::InSubquery {
expr: inner,
subquery,
..
} => {
collect_table_aliases_in_expr(inner, visitor);
collect_table_aliases_in_query(subquery, visitor);
}
Expr::BinaryOp { left, right, .. } => {
collect_table_aliases_in_expr(left, visitor);
collect_table_aliases_in_expr(right, visitor);
}
Expr::UnaryOp { expr: inner, .. }
| Expr::Nested(inner)
| Expr::Cast { expr: inner, .. } => {
collect_table_aliases_in_expr(inner, visitor);
}
Expr::Case {
operand,
conditions,
else_result,
..
} => {
if let Some(op) = operand {
collect_table_aliases_in_expr(op, visitor);
}
for cw in conditions {
collect_table_aliases_in_expr(&cw.condition, visitor);
collect_table_aliases_in_expr(&cw.result, visitor);
}
if let Some(el) = else_result {
collect_table_aliases_in_expr(el, visitor);
}
}
Expr::Function(func) => {
if let sqlparser::ast::FunctionArguments::List(arg_list) = &func.args {
for arg in &arg_list.args {
match arg {
sqlparser::ast::FunctionArg::Unnamed(
sqlparser::ast::FunctionArgExpr::Expr(e),
)
| sqlparser::ast::FunctionArg::Named {
arg: sqlparser::ast::FunctionArgExpr::Expr(e),
..
} => collect_table_aliases_in_expr(e, visitor),
_ => {}
}
}
} else if let sqlparser::ast::FunctionArguments::Subquery(query) = &func.args {
collect_table_aliases_in_query(query, visitor);
}
}
Expr::Between {
expr: inner,
low,
high,
..
} => {
collect_table_aliases_in_expr(inner, visitor);
collect_table_aliases_in_expr(low, visitor);
collect_table_aliases_in_expr(high, visitor);
}
Expr::InList {
expr: inner, list, ..
} => {
collect_table_aliases_in_expr(inner, visitor);
for item in list {
collect_table_aliases_in_expr(item, visitor);
}
}
Expr::IsNull(inner) | Expr::IsNotNull(inner) => {
collect_table_aliases_in_expr(inner, visitor);
}
_ => {}
}
}
fn join_constraint_expr(op: &sqlparser::ast::JoinOperator) -> Option<&Expr> {
use sqlparser::ast::{JoinConstraint, JoinOperator};
let constraint = match op {
JoinOperator::Join(c)
| JoinOperator::Inner(c)
| JoinOperator::Left(c)
| JoinOperator::LeftOuter(c)
| JoinOperator::Right(c)
| JoinOperator::RightOuter(c)
| JoinOperator::FullOuter(c)
| JoinOperator::CrossJoin(c)
| JoinOperator::Semi(c)
| JoinOperator::LeftSemi(c)
| JoinOperator::RightSemi(c)
| JoinOperator::Anti(c)
| JoinOperator::LeftAnti(c)
| JoinOperator::RightAnti(c)
| JoinOperator::StraightJoin(c) => c,
JoinOperator::AsOf { constraint, .. } => constraint,
JoinOperator::CrossApply | JoinOperator::OuterApply => return None,
};
if let JoinConstraint::On(expr) = constraint {
Some(expr)
} else {
None
}
}
fn table_factor_alias_ident(table_factor: &TableFactor) -> Option<&Ident> {
let alias = match table_factor {
TableFactor::Table { alias, .. }
| TableFactor::Derived { alias, .. }
| TableFactor::TableFunction { alias, .. }
| TableFactor::Function { alias, .. }
| TableFactor::UNNEST { alias, .. }
| TableFactor::JsonTable { alias, .. }
| TableFactor::OpenJsonTable { alias, .. }
| TableFactor::NestedJoin { alias, .. }
| TableFactor::Pivot { alias, .. }
| TableFactor::Unpivot { alias, .. }
| TableFactor::MatchRecognize { alias, .. }
| TableFactor::XmlTable { alias, .. }
| TableFactor::SemanticView { alias, .. } => alias.as_ref(),
}?;
Some(&alias.name)
}
fn explicit_as_before_alias_tokens(
tokens: &[LocatedToken],
alias_start: usize,
) -> Option<(bool, Option<Span>)> {
let token = tokens
.iter()
.rev()
.find(|token| token.end <= alias_start && !is_trivia_token(&token.token))?;
if is_as_token(&token.token) {
let leading_ws_start = tokens
.iter()
.rev()
.find(|t| t.end <= token.start && !is_trivia_token(&t.token))
.map(|t| t.end)
.unwrap_or(token.start);
Some((true, Some(Span::new(leading_ws_start, token.end))))
} else {
Some((false, None))
}
}
fn has_whitespace_before(tokens: &[LocatedToken], pos: usize) -> bool {
tokens
.iter()
.rev()
.find(|t| t.end <= pos)
.is_some_and(|t| is_trivia_token(&t.token))
}
fn is_as_token(token: &Token) -> bool {
match token {
Token::Word(word) => word.value.eq_ignore_ascii_case("AS"),
_ => false,
}
}
#[derive(Clone)]
struct LocatedToken {
token: Token,
start: usize,
end: usize,
}
fn tokenized(sql: &str, dialect: Dialect) -> Option<Vec<LocatedToken>> {
let dialect = dialect.to_sqlparser_dialect();
let mut tokenizer = Tokenizer::new(dialect.as_ref(), sql);
let tokens = tokenizer.tokenize_with_location().ok()?;
let mut out = Vec::with_capacity(tokens.len());
for token in tokens {
let (start, end) = token_with_span_offsets(sql, &token)?;
out.push(LocatedToken {
token: token.token,
start,
end,
});
}
Some(out)
}
fn tokenized_for_context(ctx: &LintContext) -> Option<Vec<LocatedToken>> {
let statement_start = ctx.statement_range.start;
ctx.with_document_tokens(|tokens| {
if tokens.is_empty() {
return None;
}
Some(
tokens
.iter()
.filter_map(|token| {
let (start, end) = token_with_span_offsets(ctx.sql, token)?;
if start < ctx.statement_range.start || end > ctx.statement_range.end {
return None;
}
Some(LocatedToken {
token: token.token.clone(),
start: start - statement_start,
end: end - statement_start,
})
})
.collect::<Vec<_>>(),
)
})
}
fn token_with_span_offsets(sql: &str, token: &TokenWithSpan) -> Option<(usize, usize)> {
let start = line_col_to_offset(
sql,
token.span.start.line as usize,
token.span.start.column as usize,
)?;
let end = line_col_to_offset(
sql,
token.span.end.line as usize,
token.span.end.column as usize,
)?;
Some((start, end))
}
fn is_trivia_token(token: &Token) -> bool {
matches!(
token,
Token::Whitespace(Whitespace::Space | Whitespace::Tab | Whitespace::Newline)
| Token::Whitespace(Whitespace::SingleLineComment { .. })
| Token::Whitespace(Whitespace::MultiLineComment(_))
)
}
fn line_col_to_offset(sql: &str, line: usize, column: usize) -> Option<usize> {
if line == 0 || column == 0 {
return None;
}
let mut current_line = 1usize;
let mut current_col = 1usize;
for (offset, ch) in sql.char_indices() {
if current_line == line && current_col == column {
return Some(offset);
}
if ch == '\n' {
current_line += 1;
current_col = 1;
} else {
current_col += 1;
}
}
if current_line == line && current_col == column {
return Some(sql.len());
}
None
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{
parser::{parse_sql, parse_sql_with_dialect},
types::IssueAutofixApplicability,
Dialect,
};
fn run_with_rule(sql: &str, rule: AliasingTableStyle) -> Vec<Issue> {
let stmts = parse_sql(sql).expect("parse");
stmts
.iter()
.enumerate()
.flat_map(|(index, stmt)| {
rule.check(
stmt,
&LintContext {
sql,
statement_range: 0..sql.len(),
statement_index: index,
},
)
})
.collect()
}
fn run(sql: &str) -> Vec<Issue> {
run_with_rule(sql, AliasingTableStyle::default())
}
#[test]
fn flags_implicit_table_aliases() {
let issues = run("select * from users u join orders o on u.id = o.user_id");
assert_eq!(issues.len(), 2);
assert!(issues.iter().all(|i| i.code == issue_codes::LINT_AL_001));
}
#[test]
fn allows_explicit_as_table_aliases() {
let issues = run("select * from users as u join orders as o on u.id = o.user_id");
assert!(issues.is_empty());
}
#[test]
fn flags_explicit_aliases_when_implicit_policy_requested() {
let config = LintConfig {
enabled: true,
disabled_rules: vec![],
rule_configs: std::collections::BTreeMap::from([(
"LINT_AL_001".to_string(),
serde_json::json!({"aliasing": "implicit"}),
)]),
};
let issues = run_with_rule(
"select * from users as u join orders as o on u.id = o.user_id",
AliasingTableStyle::from_config(&config),
);
assert_eq!(issues.len(), 2);
}
#[test]
fn flags_implicit_derived_table_alias() {
let issues = run("select * from (select 1) d");
assert_eq!(issues.len(), 1);
assert_eq!(issues[0].code, issue_codes::LINT_AL_001);
}
#[test]
fn flags_implicit_merge_aliases_in_bigquery() {
let sql = "MERGE dataset.inventory t USING dataset.newarrivals s ON t.product = s.product WHEN MATCHED THEN UPDATE SET quantity = t.quantity + s.quantity";
let statements = parse_sql_with_dialect(sql, Dialect::Bigquery).expect("parse");
let issues = statements
.iter()
.enumerate()
.flat_map(|(index, stmt)| {
AliasingTableStyle::default().check(
stmt,
&LintContext {
sql,
statement_range: 0..sql.len(),
statement_index: index,
},
)
})
.collect::<Vec<_>>();
assert_eq!(issues.len(), 2);
assert!(issues.iter().all(|i| i.code == issue_codes::LINT_AL_001));
}
#[test]
fn explicit_mode_emits_safe_insert_as_autofix_patch() {
let sql = "select * from users u";
let issues = run(sql);
assert_eq!(issues.len(), 1);
let autofix = issues[0]
.autofix
.as_ref()
.expect("expected AL001 core autofix metadata in explicit mode");
assert_eq!(autofix.applicability, IssueAutofixApplicability::Safe);
assert_eq!(autofix.edits.len(), 1);
assert_eq!(autofix.edits[0].replacement, "AS ");
assert_eq!(autofix.edits[0].span.start, autofix.edits[0].span.end);
}
#[test]
fn implicit_mode_emits_safe_remove_as_autofix_patch() {
let config = LintConfig {
enabled: true,
disabled_rules: vec![],
rule_configs: std::collections::BTreeMap::from([(
"LINT_AL_001".to_string(),
serde_json::json!({"aliasing": "implicit"}),
)]),
};
let rule = AliasingTableStyle::from_config(&config);
let sql = "select * from users as u";
let issues = run_with_rule(sql, rule);
assert_eq!(issues.len(), 1);
let autofix = issues[0]
.autofix
.as_ref()
.expect("expected AL001 core autofix metadata in implicit mode");
assert_eq!(autofix.applicability, IssueAutofixApplicability::Safe);
assert_eq!(autofix.edits.len(), 1);
assert_eq!(autofix.edits[0].replacement, " ");
assert_eq!(
&sql[autofix.edits[0].span.start..autofix.edits[0].span.end],
" as "
);
}
#[test]
fn flags_implicit_aliases_in_update_where_exists_subquery() {
let sql = "UPDATE t SET x = 1 WHERE EXISTS (SELECT 1 FROM users u)";
let issues = run(sql);
assert_eq!(issues.len(), 1);
assert_eq!(issues[0].code, issue_codes::LINT_AL_001);
}
#[test]
fn skips_update_target_table_alias() {
let sql = "UPDATE users u SET u.x = 1";
let issues = run(sql);
assert!(issues.is_empty());
}
#[test]
fn flags_implicit_aliases_in_delete_where_exists() {
let sql =
"DELETE FROM users u WHERE EXISTS (SELECT 1 FROM orders o WHERE o.user_id = u.id)";
let issues = run(sql);
assert_eq!(issues.len(), 2);
}
}