use hashbrown::HashMap;
use itertools::Itertools;
use regex::Regex;
use smol_str::SmolStr;
use sqruff_lib_core::dialects::common::{AliasInfo, ColumnAliasInfo};
use sqruff_lib_core::dialects::syntax::{SyntaxKind, SyntaxSet};
use sqruff_lib_core::parser::segments::ErasedSegment;
use sqruff_lib_core::parser::segments::object_reference::ObjectReferenceSegment;
use sqruff_lib_core::utils::analysis::select::get_select_statement_info;
use crate::core::config::Value;
use crate::core::rules::context::RuleContext;
use crate::core::rules::crawlers::{Crawler, SegmentSeekerCrawler};
use crate::core::rules::{Erased as _, ErasedRule, LintResult, Rule, RuleGroups};
use crate::rules::aliasing::al04::RuleAL04;
#[derive(Clone, Debug, Default)]
pub struct RuleRF02Config {
ignore_words: Vec<String>,
ignore_words_regex: Vec<Regex>,
subqueries_ignore_external_references: bool,
}
#[derive(Clone, Debug)]
pub struct RuleRF02 {
base: RuleAL04<RuleRF02Config>,
}
impl Default for RuleRF02 {
fn default() -> Self {
Self {
base: RuleAL04 {
lint_references_and_aliases: Self::lint_references_and_aliases,
context: RuleRF02Config::default(),
},
}
}
}
impl Rule for RuleRF02 {
fn load_from_config(&self, config: &HashMap<String, Value>) -> Result<ErasedRule, String> {
let ignore_words = config["ignore_words"]
.map(|it| {
it.as_array()
.unwrap()
.iter()
.map(|it| it.as_string().unwrap().to_lowercase())
.collect()
})
.unwrap_or_default();
let ignore_words_regex = config["ignore_words_regex"]
.map(|it| {
it.as_array()
.unwrap()
.iter()
.map(|it| Regex::new(it.as_string().unwrap()).unwrap())
.collect()
})
.unwrap_or_default();
let subqueries_ignore_external_references = config["subqueries_ignore_external_references"]
.as_bool()
.unwrap_or(false);
Ok(Self {
base: RuleAL04 {
lint_references_and_aliases: Self::lint_references_and_aliases,
context: RuleRF02Config {
ignore_words,
ignore_words_regex,
subqueries_ignore_external_references,
},
},
}
.erased())
}
fn name(&self) -> &'static str {
"references.qualification"
}
fn description(&self) -> &'static str {
"References should be qualified if select has more than one referenced table/view."
}
fn long_description(&self) -> &'static str {
r"
**Anti-pattern**
In this example, the reference `vee` has not been declared, and the variables `a` and `b` are potentially ambiguous.
```sql
SELECT a, b
FROM foo
LEFT JOIN vee ON vee.a = foo.a
```
**Best practice**
Add the references.
```sql
SELECT foo.a, vee.b
FROM foo
LEFT JOIN vee ON vee.a = foo.a
```
"
}
fn groups(&self) -> &'static [RuleGroups] {
&[RuleGroups::All, RuleGroups::References]
}
fn eval(&self, context: &RuleContext) -> Vec<LintResult> {
self.base.eval(context)
}
fn crawl_behaviour(&self) -> Crawler {
SegmentSeekerCrawler::new(const { SyntaxSet::new(&[SyntaxKind::SelectStatement]) }).into()
}
}
impl RuleRF02 {
fn is_root_from_clause(rule_context: &RuleContext) -> bool {
for x in rule_context.parent_stack.iter().rev() {
if x.is_type(SyntaxKind::FromClause) {
return true;
} else if x.is_type(SyntaxKind::WhereClause) {
return false;
}
}
false
}
#[allow(clippy::too_many_arguments)]
fn lint_references_and_aliases(
mut table_aliases: Vec<AliasInfo>,
standalone_aliases: Vec<SmolStr>,
references: Vec<ObjectReferenceSegment>,
col_aliases: Vec<ColumnAliasInfo>,
using_cols: Vec<SmolStr>,
parent_select: Option<ErasedSegment>,
rule_context: &RuleContext,
context: &RuleRF02Config,
) -> Vec<LintResult> {
let parent_select_info = parent_select.and_then(|parent| {
get_select_statement_info(&parent, rule_context.dialect.into(), true)
});
if let Some(parent_select_info) = parent_select_info {
for table_alias in parent_select_info.table_aliases {
let is_from = Self::is_root_from_clause(rule_context);
if !table_alias
.from_expression_element
.path_to(&rule_context.segment)
.is_empty()
|| is_from
|| context.subqueries_ignore_external_references
{
continue;
}
table_aliases.push(table_alias);
}
}
if table_aliases.len() <= 1 {
return Vec::new();
}
let mut violation_buff = Vec::new();
for r in references {
if context.ignore_words.contains(&r.0.raw().to_lowercase()) {
continue;
}
if context
.ignore_words_regex
.iter()
.any(|regex| regex.is_match(r.0.raw().as_ref()))
{
continue;
}
let this_ref_type = r.qualification();
let col_alias_names = col_aliases
.iter()
.filter_map(|c| {
if !c.column_reference_segments.contains(&r.0) {
Some(c.alias_identifier_name.as_str())
} else {
None
}
})
.collect_vec();
if this_ref_type == "unqualified"
&& !col_alias_names.contains(&r.0.raw().as_ref())
&& !using_cols.contains(r.0.raw())
&& !standalone_aliases.contains(r.0.raw())
{
violation_buff.push(LintResult::new(
r.0.clone().into(),
Vec::new(),
format!(
"Unqualified reference {} found in select with more than one referenced \
table/view.",
r.0.raw()
)
.into(),
None,
));
}
}
violation_buff
}
}