use crate::generated::NormalizationStrategy;
use crate::linter::config::LintConfig;
use crate::linter::rule::{LintContext, LintRule};
use crate::types::{issue_codes, Issue, IssueAutofixApplicability, IssuePatchEdit, Span};
use regex::Regex;
use sqlparser::ast::{Expr, Ident, SelectItem, Statement};
use sqlparser::tokenizer::{Token, TokenWithSpan, Tokenizer, Whitespace};
use super::semantic_helpers::visit_selects_in_statement;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum AliasCaseCheck {
Dialect,
CaseInsensitive,
QuotedCsNakedUpper,
QuotedCsNakedLower,
CaseSensitive,
}
impl AliasCaseCheck {
fn from_config(config: &LintConfig) -> Self {
match config
.rule_option_str(issue_codes::LINT_AL_009, "alias_case_check")
.unwrap_or("dialect")
.to_ascii_lowercase()
.as_str()
{
"case_insensitive" => Self::CaseInsensitive,
"quoted_cs_naked_upper" => Self::QuotedCsNakedUpper,
"quoted_cs_naked_lower" => Self::QuotedCsNakedLower,
"case_sensitive" => Self::CaseSensitive,
_ => Self::Dialect,
}
}
}
#[derive(Clone, Copy, Debug)]
struct NameRef<'a> {
name: &'a str,
quoted: bool,
}
pub struct AliasingSelfAliasColumn {
alias_case_check: AliasCaseCheck,
}
impl AliasingSelfAliasColumn {
pub fn from_config(config: &LintConfig) -> Self {
Self {
alias_case_check: AliasCaseCheck::from_config(config),
}
}
}
impl Default for AliasingSelfAliasColumn {
fn default() -> Self {
Self {
alias_case_check: AliasCaseCheck::Dialect,
}
}
}
impl LintRule for AliasingSelfAliasColumn {
fn code(&self) -> &'static str {
issue_codes::LINT_AL_009
}
fn name(&self) -> &'static str {
"Self alias column"
}
fn description(&self) -> &'static str {
"Column aliases should not alias to itself, i.e. self-alias."
}
fn check(&self, statement: &Statement, ctx: &LintContext) -> Vec<Issue> {
let mut violating_aliases = Vec::new();
let strategy = match self.alias_case_check {
AliasCaseCheck::Dialect => Some(ctx.dialect().normalization_strategy()),
_ => None,
};
visit_selects_in_statement(statement, &mut |select| {
for item in &select.projection {
let SelectItem::ExprWithAlias { expr, alias } = item else {
continue;
};
if aliases_expression_to_itself(expr, alias, self.alias_case_check, strategy) {
violating_aliases.push(alias.clone());
}
}
});
let violation_count = violating_aliases.len();
let mut autofix_candidates = al009_autofix_candidates_for_context(ctx, &violating_aliases);
autofix_candidates.sort_by_key(|candidate| candidate.span.start);
let candidates_align = autofix_candidates.len() == violation_count;
let legacy_candidates =
legacy_self_alias_candidates_for_context(ctx, self.alias_case_check, strategy);
if !legacy_candidates.is_empty()
&& (violation_count == 0
|| !candidates_align
|| contains_assignment_alias_pattern(ctx.statement_sql()))
{
return vec![Issue::info(
issue_codes::LINT_AL_009,
"Column aliases should not alias to itself.",
)
.with_statement(ctx.statement_index)
.with_span(legacy_candidates[0].span)
.with_autofix_edits(
IssueAutofixApplicability::Safe,
legacy_candidates
.into_iter()
.flat_map(|candidate| candidate.edits)
.collect(),
)];
}
(0..violation_count)
.map(|index| {
let mut issue = Issue::info(
issue_codes::LINT_AL_009,
"Column aliases should not alias to itself.",
)
.with_statement(ctx.statement_index);
if candidates_align {
let candidate = &autofix_candidates[index];
issue = issue.with_span(candidate.span).with_autofix_edits(
IssueAutofixApplicability::Safe,
candidate.edits.clone(),
);
}
issue
})
.collect()
}
}
#[derive(Clone, Debug)]
struct PositionedToken {
token: Token,
start: usize,
end: usize,
}
#[derive(Clone, Debug)]
struct Al009AutofixCandidate {
span: Span,
edits: Vec<IssuePatchEdit>,
}
fn al009_autofix_candidates_for_context(
ctx: &LintContext,
aliases: &[Ident],
) -> Vec<Al009AutofixCandidate> {
if aliases.is_empty() {
return Vec::new();
}
let tokens = statement_positioned_tokens(ctx);
if tokens.is_empty() {
return Vec::new();
}
let mut candidates = Vec::new();
for alias in aliases {
let Some((alias_start, alias_end)) = ident_span_offsets(ctx.sql, alias) else {
continue;
};
if alias_start < ctx.statement_range.start || alias_end > ctx.statement_range.end {
continue;
}
let Some(alias_token_index) = tokens
.iter()
.position(|token| token.start == alias_start && token.end == alias_end)
else {
continue;
};
let Some(removal_span) = alias_removal_span(&tokens, alias_token_index) else {
continue;
};
candidates.push(Al009AutofixCandidate {
span: Span::new(alias_start, alias_end),
edits: vec![IssuePatchEdit::new(removal_span, "")],
});
}
candidates
}
fn statement_positioned_tokens(ctx: &LintContext) -> Vec<PositionedToken> {
let from_document_tokens = ctx.with_document_tokens(|tokens| {
if tokens.is_empty() {
return None;
}
let mut positioned = Vec::new();
for token in tokens {
let (start, end) = token_with_span_offsets(ctx.sql, token)?;
if start < ctx.statement_range.start || end > ctx.statement_range.end {
continue;
}
positioned.push(PositionedToken {
token: token.token.clone(),
start,
end,
});
}
Some(positioned)
});
if let Some(tokens) = from_document_tokens {
return tokens;
}
let dialect = ctx.dialect().to_sqlparser_dialect();
let mut tokenizer = Tokenizer::new(dialect.as_ref(), ctx.statement_sql());
let Ok(tokens) = tokenizer.tokenize_with_location() else {
return Vec::new();
};
let mut positioned = Vec::new();
for token in &tokens {
let Some((start, end)) = token_with_span_offsets(ctx.statement_sql(), token) else {
continue;
};
positioned.push(PositionedToken {
token: token.token.clone(),
start: ctx.statement_range.start + start,
end: ctx.statement_range.start + end,
});
}
positioned
}
fn alias_removal_span(tokens: &[PositionedToken], alias_token_index: usize) -> Option<Span> {
let alias = &tokens[alias_token_index];
let previous_non_trivia = previous_non_trivia_index(tokens, alias_token_index)?;
if token_is_as_keyword(&tokens[previous_non_trivia].token) {
let expression_token = previous_non_trivia_index(tokens, previous_non_trivia)?;
let gap_start = expression_token + 1;
if gap_start > previous_non_trivia
|| trivia_contains_comment(tokens, gap_start, previous_non_trivia)
|| trivia_contains_comment(tokens, previous_non_trivia + 1, alias_token_index)
{
return None;
}
return Some(Span::new(tokens[gap_start].start, alias.end));
}
let gap_start = previous_non_trivia + 1;
if gap_start >= alias_token_index
|| trivia_contains_comment(tokens, gap_start, alias_token_index)
{
return None;
}
Some(Span::new(tokens[gap_start].start, alias.end))
}
fn previous_non_trivia_index(tokens: &[PositionedToken], before: usize) -> Option<usize> {
if before == 0 {
return None;
}
let mut index = before - 1;
loop {
if !is_trivia(&tokens[index].token) {
return Some(index);
}
if index == 0 {
return None;
}
index -= 1;
}
}
fn trivia_contains_comment(tokens: &[PositionedToken], start: usize, end: usize) -> bool {
if start >= end {
return false;
}
tokens[start..end].iter().any(|token| {
matches!(
token.token,
Token::Whitespace(
Whitespace::SingleLineComment { .. } | Whitespace::MultiLineComment(_)
)
)
})
}
fn token_is_as_keyword(token: &Token) -> bool {
matches!(token, Token::Word(word) if word.value.eq_ignore_ascii_case("AS"))
}
fn is_trivia(token: &Token) -> bool {
matches!(
token,
Token::Whitespace(
Whitespace::Space
| Whitespace::Newline
| Whitespace::Tab
| Whitespace::SingleLineComment { .. }
| Whitespace::MultiLineComment(_)
)
)
}
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 ident_span_offsets(sql: &str, ident: &Ident) -> Option<(usize, usize)> {
let start = line_col_to_offset(
sql,
ident.span.start.line as usize,
ident.span.start.column as usize,
)?;
let end = line_col_to_offset(
sql,
ident.span.end.line as usize,
ident.span.end.column as usize,
)?;
Some((start, end))
}
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
}
fn aliases_expression_to_itself(
expr: &Expr,
alias: &Ident,
alias_case_check: AliasCaseCheck,
dialect_strategy: Option<NormalizationStrategy>,
) -> bool {
let Some(source_name) = expression_name(expr) else {
return false;
};
let alias_name = NameRef {
name: alias.value.as_str(),
quoted: alias.quote_style.is_some(),
};
names_match(source_name, alias_name, alias_case_check, dialect_strategy)
}
fn expression_name(expr: &Expr) -> Option<NameRef<'_>> {
match expr {
Expr::Identifier(identifier) => Some(NameRef {
name: identifier.value.as_str(),
quoted: identifier.quote_style.is_some(),
}),
Expr::CompoundIdentifier(parts) => parts.last().map(|part| NameRef {
name: part.value.as_str(),
quoted: part.quote_style.is_some(),
}),
Expr::Nested(inner) => expression_name(inner),
_ => None,
}
}
fn names_match(
left: NameRef<'_>,
right: NameRef<'_>,
alias_case_check: AliasCaseCheck,
dialect_strategy: Option<NormalizationStrategy>,
) -> bool {
match alias_case_check {
AliasCaseCheck::CaseInsensitive => left.name.eq_ignore_ascii_case(right.name),
AliasCaseCheck::CaseSensitive => left.name == right.name,
AliasCaseCheck::Dialect => {
let strategy = dialect_strategy.unwrap_or(NormalizationStrategy::CaseInsensitive);
if left.quoted != right.quoted {
return false;
}
if left.quoted {
left.name == right.name
} else {
match strategy {
NormalizationStrategy::CaseSensitive => left.name == right.name,
NormalizationStrategy::CaseInsensitive
| NormalizationStrategy::Lowercase
| NormalizationStrategy::Uppercase => {
left.name.eq_ignore_ascii_case(right.name)
}
}
}
}
AliasCaseCheck::QuotedCsNakedUpper | AliasCaseCheck::QuotedCsNakedLower => {
normalize_name_for_mode(left, alias_case_check)
== normalize_name_for_mode(right, alias_case_check)
}
}
}
fn normalize_name_for_mode(name_ref: NameRef<'_>, mode: AliasCaseCheck) -> String {
match mode {
AliasCaseCheck::QuotedCsNakedUpper => {
if name_ref.quoted {
name_ref.name.to_string()
} else {
name_ref.name.to_ascii_uppercase()
}
}
AliasCaseCheck::QuotedCsNakedLower => {
if name_ref.quoted {
name_ref.name.to_string()
} else {
name_ref.name.to_ascii_lowercase()
}
}
_ => name_ref.name.to_string(),
}
}
fn legacy_self_alias_candidates_for_context(
ctx: &LintContext,
alias_case_check: AliasCaseCheck,
dialect_strategy: Option<NormalizationStrategy>,
) -> Vec<Al009AutofixCandidate> {
let sql = ctx.statement_sql();
let Ok(select_clause_regex) = Regex::new(r"(?is)\bselect\b(?P<clause>.*?)\bfrom\b") else {
return Vec::new();
};
let Some(captures) = select_clause_regex.captures(sql) else {
return Vec::new();
};
let Some(clause) = captures.name("clause") else {
return Vec::new();
};
let clause_start = clause.start();
let clause_sql = clause.as_str();
let mut line_offset = 0usize;
let mut candidates = Vec::new();
for line in clause_sql.split_inclusive('\n') {
let line_no_newline = line.strip_suffix('\n').unwrap_or(line);
let mut content_start = 0usize;
while content_start < line_no_newline.len()
&& line_no_newline.as_bytes()[content_start].is_ascii_whitespace()
{
content_start += 1;
}
let mut content_end = line_no_newline.len();
while content_end > content_start
&& line_no_newline.as_bytes()[content_end - 1].is_ascii_whitespace()
{
content_end -= 1;
}
if content_end > content_start && line_no_newline.as_bytes()[content_end - 1] == b',' {
content_end -= 1;
}
while content_end > content_start
&& line_no_newline.as_bytes()[content_end - 1].is_ascii_whitespace()
{
content_end -= 1;
}
if content_end <= content_start {
line_offset += line.len();
continue;
}
let content = &line_no_newline[content_start..content_end];
let Some(replacement) = legacy_self_alias_replacement(
content,
ctx.dialect(),
alias_case_check,
dialect_strategy,
) else {
line_offset += line.len();
continue;
};
if replacement == content {
line_offset += line.len();
continue;
}
let edit_start = clause_start + line_offset + content_start;
let edit_end = clause_start + line_offset + content_end;
let span = ctx.span_from_statement_offset(edit_start, edit_end);
candidates.push(Al009AutofixCandidate {
span,
edits: vec![IssuePatchEdit::new(span, replacement)],
});
line_offset += line.len();
}
candidates
}
fn legacy_self_alias_replacement(
target: &str,
dialect: crate::types::Dialect,
alias_case_check: AliasCaseCheck,
dialect_strategy: Option<NormalizationStrategy>,
) -> Option<String> {
if dialect == crate::types::Dialect::Bigquery
&& target.starts_with('`')
&& target.ends_with('`')
{
let inner = &target[1..target.len().saturating_sub(1)];
if let Some(split_at) = inner.find("``") {
let left = &inner[..split_at];
let right = &inner[split_at + 2..];
if !left.is_empty() && left == right {
return Some(format!("`{left}`"));
}
}
}
if let Some(eq_pos) = target.find('=') {
let prev = eq_pos
.checked_sub(1)
.and_then(|idx| target.as_bytes().get(idx).copied());
let next = target.as_bytes().get(eq_pos + 1).copied();
if !matches!(prev, Some(b'!') | Some(b'<') | Some(b'>')) && !matches!(next, Some(b'=')) {
let alias_raw = target[..eq_pos].trim();
let expr_raw = target[eq_pos + 1..].trim();
if let (Some(expr_name), Some(alias_name)) = (
parse_identifier_name(expr_raw),
parse_identifier_name(alias_raw),
) {
if names_match(expr_name, alias_name, alias_case_check, dialect_strategy) {
return Some(expr_raw.to_string());
}
}
}
}
let upper = target.to_ascii_uppercase();
if let Some(as_pos) = upper.find(" AS ") {
let expr_raw = target[..as_pos].trim();
let alias_raw = target[as_pos + 4..].trim();
if let (Some(expr_name), Some(alias_name)) = (
parse_identifier_name(expr_raw),
parse_identifier_name(alias_raw),
) {
if names_match(expr_name, alias_name, alias_case_check, dialect_strategy) {
return Some(expr_raw.to_string());
}
}
}
let mut parts = target.split_whitespace();
let first = parts.next()?;
let second = parts.next()?;
if parts.next().is_none() {
if let (Some(expr_name), Some(alias_name)) =
(parse_identifier_name(first), parse_identifier_name(second))
{
if names_match(expr_name, alias_name, alias_case_check, dialect_strategy) {
return Some(first.to_string());
}
}
}
None
}
fn parse_identifier_name(raw: &str) -> Option<NameRef<'_>> {
if raw.len() >= 2 {
let bytes = raw.as_bytes();
if (bytes[0] == b'"' && bytes[raw.len() - 1] == b'"')
|| (bytes[0] == b'`' && bytes[raw.len() - 1] == b'`')
|| (bytes[0] == b'[' && bytes[raw.len() - 1] == b']')
{
return Some(NameRef {
name: &raw[1..raw.len() - 1],
quoted: true,
});
}
}
let mut chars = raw.chars();
let first = chars.next()?;
if !(first.is_ascii_alphabetic() || first == '_') {
return None;
}
if !chars.all(|ch| ch.is_ascii_alphanumeric() || matches!(ch, '_' | '$')) {
return None;
}
Some(NameRef {
name: raw,
quoted: false,
})
}
fn contains_assignment_alias_pattern(sql: &str) -> bool {
let Ok(pattern) =
Regex::new(r"(?im)^\s*[A-Za-z_][A-Za-z0-9_$]*\s*=\s*[A-Za-z_][A-Za-z0-9_$]*\s*,?\s*$")
else {
return false;
};
pattern.is_match(sql)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::linter::rule::with_active_dialect;
use crate::parser::{parse_sql, parse_sql_with_dialect};
use crate::types::{Dialect, IssueAutofixApplicability};
fn run(sql: &str) -> Vec<Issue> {
let statements = parse_sql(sql).expect("parse");
let rule = AliasingSelfAliasColumn::default();
statements
.iter()
.enumerate()
.flat_map(|(index, statement)| {
rule.check(
statement,
&LintContext {
sql,
statement_range: 0..sql.len(),
statement_index: index,
},
)
})
.collect()
}
fn run_in_dialect(sql: &str, dialect: Dialect) -> Vec<Issue> {
let statements = parse_sql_with_dialect(sql, dialect).expect("parse");
let rule = AliasingSelfAliasColumn::default();
let mut issues = Vec::new();
with_active_dialect(dialect, || {
for (index, statement) in statements.iter().enumerate() {
issues.extend(rule.check(
statement,
&LintContext {
sql,
statement_range: 0..sql.len(),
statement_index: index,
},
));
}
});
issues
}
fn apply_issue_autofix(sql: &str, issue: &Issue) -> Option<String> {
let autofix = issue.autofix.as_ref()?;
let mut out = sql.to_string();
let mut edits = autofix.edits.clone();
edits.sort_by_key(|edit| (edit.span.start, edit.span.end));
for edit in edits.into_iter().rev() {
out.replace_range(edit.span.start..edit.span.end, &edit.replacement);
}
Some(out)
}
#[test]
fn flags_plain_self_alias() {
let issues = run("SELECT a AS a FROM t");
assert_eq!(issues.len(), 1);
assert_eq!(issues[0].code, issue_codes::LINT_AL_009);
}
#[test]
fn flags_qualified_self_alias() {
let issues = run("SELECT t.a AS a FROM t");
assert_eq!(issues.len(), 1);
}
#[test]
fn flags_case_insensitive_self_alias() {
let issues = run("SELECT a AS A FROM t");
assert_eq!(issues.len(), 1);
}
#[test]
fn does_not_flag_distinct_alias_name() {
let issues = run("SELECT a AS b FROM t");
assert!(issues.is_empty());
}
#[test]
fn does_not_flag_non_identifier_expression() {
let issues = run("SELECT a + 1 AS a FROM t");
assert!(issues.is_empty());
}
#[test]
fn default_dialect_mode_does_not_flag_quoted_case_mismatch() {
let issues = run("SELECT \"A\" AS a FROM t");
assert!(issues.is_empty());
}
#[test]
fn default_dialect_mode_flags_exact_quoted_match() {
let issues = run("SELECT \"A\" AS \"A\" FROM t");
assert_eq!(issues.len(), 1);
}
#[test]
fn alias_case_check_case_sensitive_respects_case() {
let sql = "SELECT a AS A FROM t";
let statements = parse_sql(sql).expect("parse");
let rule = AliasingSelfAliasColumn::from_config(&LintConfig {
enabled: true,
disabled_rules: vec![],
rule_configs: std::collections::BTreeMap::from([(
"aliasing.self_alias.column".to_string(),
serde_json::json!({"alias_case_check": "case_sensitive"}),
)]),
});
let issues = rule.check(
&statements[0],
&LintContext {
sql,
statement_range: 0..sql.len(),
statement_index: 0,
},
);
assert!(issues.is_empty());
}
#[test]
fn alias_case_check_quoted_cs_naked_upper_flags_upper_fold_match() {
let sql = "SELECT \"FOO\" AS foo FROM t";
let statements = parse_sql(sql).expect("parse");
let rule = AliasingSelfAliasColumn::from_config(&LintConfig {
enabled: true,
disabled_rules: vec![],
rule_configs: std::collections::BTreeMap::from([(
"aliasing.self_alias.column".to_string(),
serde_json::json!({"alias_case_check": "quoted_cs_naked_upper"}),
)]),
});
let issues = rule.check(
&statements[0],
&LintContext {
sql,
statement_range: 0..sql.len(),
statement_index: 0,
},
);
assert_eq!(issues.len(), 1);
}
#[test]
fn alias_case_check_quoted_cs_naked_upper_allows_nonmatching_quoted_case() {
let sql = "SELECT \"foo\" AS foo FROM t";
let statements = parse_sql(sql).expect("parse");
let rule = AliasingSelfAliasColumn::from_config(&LintConfig {
enabled: true,
disabled_rules: vec![],
rule_configs: std::collections::BTreeMap::from([(
"aliasing.self_alias.column".to_string(),
serde_json::json!({"alias_case_check": "quoted_cs_naked_upper"}),
)]),
});
let issues = rule.check(
&statements[0],
&LintContext {
sql,
statement_range: 0..sql.len(),
statement_index: 0,
},
);
assert!(issues.is_empty());
}
#[test]
fn alias_case_check_quoted_cs_naked_lower_flags_lower_fold_match() {
let sql = "SELECT \"foo\" AS FOO FROM t";
let statements = parse_sql(sql).expect("parse");
let rule = AliasingSelfAliasColumn::from_config(&LintConfig {
enabled: true,
disabled_rules: vec![],
rule_configs: std::collections::BTreeMap::from([(
"aliasing.self_alias.column".to_string(),
serde_json::json!({"alias_case_check": "quoted_cs_naked_lower"}),
)]),
});
let issues = rule.check(
&statements[0],
&LintContext {
sql,
statement_range: 0..sql.len(),
statement_index: 0,
},
);
assert_eq!(issues.len(), 1);
}
#[test]
fn alias_case_check_quoted_cs_naked_lower_allows_nonmatching_quoted_case() {
let sql = "SELECT \"FOO\" AS FOO FROM t";
let statements = parse_sql(sql).expect("parse");
let rule = AliasingSelfAliasColumn::from_config(&LintConfig {
enabled: true,
disabled_rules: vec![],
rule_configs: std::collections::BTreeMap::from([(
"aliasing.self_alias.column".to_string(),
serde_json::json!({"alias_case_check": "quoted_cs_naked_lower"}),
)]),
});
let issues = rule.check(
&statements[0],
&LintContext {
sql,
statement_range: 0..sql.len(),
statement_index: 0,
},
);
assert!(issues.is_empty());
}
#[test]
fn self_alias_with_as_emits_safe_autofix_patch() {
let sql = "SELECT a AS a FROM t";
let issues = run(sql);
assert_eq!(issues.len(), 1);
let autofix = issues[0]
.autofix
.as_ref()
.expect("expected AL009 core autofix metadata");
assert_eq!(autofix.applicability, IssueAutofixApplicability::Safe);
assert_eq!(autofix.edits.len(), 1);
let edit = &autofix.edits[0];
assert_eq!(&sql[edit.span.start..edit.span.end], " AS a");
assert_eq!(edit.replacement, "");
}
#[test]
fn self_alias_without_as_emits_safe_autofix_patch() {
let sql = "SELECT a a FROM t";
let issues = run(sql);
assert_eq!(issues.len(), 1);
let autofix = issues[0]
.autofix
.as_ref()
.expect("expected AL009 core autofix metadata");
assert_eq!(autofix.applicability, IssueAutofixApplicability::Safe);
assert_eq!(autofix.edits.len(), 1);
let edit = &autofix.edits[0];
assert_eq!(&sql[edit.span.start..edit.span.end], " a");
assert_eq!(edit.replacement, "");
}
#[test]
fn clickhouse_case_sensitive_no_false_positives() {
let sql = "select col_b as Col_B, COL_C as col_c, Col_D as COL_D from foo";
let issues = run_in_dialect(sql, Dialect::Clickhouse);
assert!(issues.is_empty());
}
#[test]
fn clickhouse_quoted_case_sensitive_no_false_positives() {
let sql = r#"select "col_b" as "Col_B", "COL_C" as "col_c", "Col_D" as "COL_D" from foo"#;
let issues = run_in_dialect(sql, Dialect::Clickhouse);
assert!(issues.is_empty());
}
#[test]
fn different_quotes_not_flagged() {
let sql = r#"select "col_b" as col_b, COL_C as "COL_C", "Col_D" as Col_D from foo"#;
let issues = run_in_dialect(sql, Dialect::Ansi);
assert!(issues.is_empty());
}
#[test]
fn bigquery_backtick_self_alias_detected() {
let sql = "SELECT `col`as`col` FROM clients as c";
let issues = run_in_dialect(sql, Dialect::Bigquery);
assert_eq!(issues.len(), 1);
}
#[test]
fn tsql_self_alias_assignments_use_legacy_fallback_fix() {
let sql = "select\n this_alias_is_fine = col_a,\n col_b = col_b,\n COL_C AS COL_C,\n Col_D = Col_D,\n col_e col_e,\n COL_F COL_F,\n Col_G Col_G\nfrom foo";
let statements = parse_sql("SELECT 1").expect("synthetic parse");
let rule = AliasingSelfAliasColumn::default();
let issues = with_active_dialect(Dialect::Mssql, || {
rule.check(
&statements[0],
&LintContext {
sql,
statement_range: 0..sql.len(),
statement_index: 0,
},
)
});
assert_eq!(issues.len(), 1);
let fixed = apply_issue_autofix(sql, &issues[0]).expect("apply autofix");
assert_eq!(
fixed,
"select\n this_alias_is_fine = col_a,\n col_b,\n COL_C,\n Col_D,\n col_e,\n COL_F,\n Col_G\nfrom foo"
);
}
#[test]
fn bigquery_adjacent_backtick_self_alias_uses_legacy_fallback_fix() {
let sql = "SELECT `col``col`\nFROM clients as c";
let statements = parse_sql("SELECT 1").expect("synthetic parse");
let rule = AliasingSelfAliasColumn::default();
let issues = with_active_dialect(Dialect::Bigquery, || {
rule.check(
&statements[0],
&LintContext {
sql,
statement_range: 0..sql.len(),
statement_index: 0,
},
)
});
assert_eq!(issues.len(), 1);
let fixed = apply_issue_autofix(sql, &issues[0]).expect("apply autofix");
assert_eq!(fixed, "SELECT `col`\nFROM clients as c");
}
#[test]
fn tsql_parsed_statement_still_gets_self_alias_autofix() {
let sql = "select\n this_alias_is_fine = col_a,\n col_b = col_b,\n COL_C AS COL_C,\n Col_D = Col_D,\n col_e col_e,\n COL_F COL_F,\n Col_G Col_G\nfrom foo";
let issues = run_in_dialect(sql, Dialect::Mssql);
assert_eq!(issues.len(), 1);
let fixed = apply_issue_autofix(sql, &issues[0]).expect("apply autofix");
assert_eq!(
fixed,
"select\n this_alias_is_fine = col_a,\n col_b,\n COL_C,\n Col_D,\n col_e,\n COL_F,\n Col_G\nfrom foo"
);
}
}