diff --git a/crates/lib/src/core/config.rs b/crates/lib/src/core/config.rs index 1b170fff9..180b9b6b2 100644 --- a/crates/lib/src/core/config.rs +++ b/crates/lib/src/core/config.rs @@ -543,7 +543,10 @@ impl Value { pub fn as_array(&self) -> Option> { match self { Self::Array(v) => Some(v.clone()), - v @ Self::String(_) => Some(vec![v.clone()]), + Self::String(q) => { + let xs = q.split(',').map(|it| Value::String(it.into())).collect_vec(); + Some(xs) + } _ => None, } } diff --git a/crates/lib/src/core/parser/segments/base.rs b/crates/lib/src/core/parser/segments/base.rs index 7c28f64f1..d2a5e7e23 100644 --- a/crates/lib/src/core/parser/segments/base.rs +++ b/crates/lib/src/core/parser/segments/base.rs @@ -19,7 +19,7 @@ use crate::core::parser::markers::PositionMarker; use crate::core::parser::segments::fix::{AnchorEditInfo, FixPatch, SourceFix}; use crate::core::rules::base::{EditType, LintFix}; use crate::core::templaters::base::TemplatedFile; -use crate::dialects::ansi::ObjectReferenceSegment; +use crate::dialects::ansi::{ObjectReferenceKind, ObjectReferenceSegment}; use crate::helpers::ToErasedSegment; #[derive(Debug, Clone)] @@ -123,7 +123,14 @@ impl ErasedSegment { } pub fn reference(&self) -> ObjectReferenceSegment { - ObjectReferenceSegment(self.clone()) + ObjectReferenceSegment( + self.clone(), + match self.get_type() { + "table_reference" => ObjectReferenceKind::Table, + "wildcard_identifier" => ObjectReferenceKind::WildcardIdentifier, + _ => ObjectReferenceKind::Object, + }, + ) } pub fn recursive_crawl_all(&self, reverse: bool) -> Vec { diff --git a/crates/lib/src/dialects/ansi.rs b/crates/lib/src/dialects/ansi.rs index 5534254cd..959818175 100644 --- a/crates/lib/src/dialects/ansi.rs +++ b/crates/lib/src/dialects/ansi.rs @@ -3,8 +3,8 @@ use std::fmt::Debug; use std::sync::{Arc, OnceLock}; use ahash::AHashSet; -use itertools::Itertools; -use smol_str::SmolStr; +use itertools::{enumerate, Itertools}; +use smol_str::{SmolStr, ToSmolStr}; use uuid::Uuid; use super::ansi_keywords::{ANSI_RESERVED_KEYWORDS, ANSI_UNRESERVED_KEYWORDS}; @@ -6048,7 +6048,14 @@ pub struct ObjectReferencePart { } #[derive(Clone)] -pub struct ObjectReferenceSegment(pub ErasedSegment); +pub struct ObjectReferenceSegment(pub ErasedSegment, pub ObjectReferenceKind); + +#[derive(Clone)] +pub enum ObjectReferenceKind { + Object, + Table, + WildcardIdentifier, +} impl ObjectReferenceSegment { pub fn is_qualified(&self) -> bool { @@ -6116,13 +6123,81 @@ impl ObjectReferenceSegment { } pub fn iter_raw_references(&self) -> Vec { - let mut acc = Vec::new(); + match self.1 { + ObjectReferenceKind::Object => { + let mut acc = Vec::new(); - for elem in self.0.recursive_crawl(&["identifier", "naked_identifier"], true, None, true) { - acc.extend(self.iter_reference_parts(elem)); - } + for elem in + self.0.recursive_crawl(&["identifier", "naked_identifier"], true, None, true) + { + acc.extend(self.iter_reference_parts(elem)); + } - acc + acc + } + ObjectReferenceKind::Table => { + let mut acc = Vec::new(); + let mut parts = Vec::new(); + let mut elems_for_parts = Vec::new(); + + let mut flush = + |parts: &mut Vec, elems_for_parts: &mut Vec| { + acc.push(ObjectReferencePart { + part: std::mem::take(parts).iter().join(""), + segments: std::mem::take(elems_for_parts), + }); + }; + + for elem in self.0.recursive_crawl( + &["identifier", "naked_identifier", "literal", "dash", "dot", "star"], + true, + None, + true, + ) { + if !elem.is_type("dot") { + if elem.is_type("identifier") || elem.is_type("naked_identifier") { + let elem_raw = elem.raw(); + let elem_subparts = elem_raw.split(".").collect_vec(); + let elem_subparts_count = elem_subparts.len(); + + for (idx, part) in enumerate(elem_subparts) { + parts.push(part.to_smolstr()); + elems_for_parts.push(elem.clone()); + + if idx != elem_subparts_count - 1 { + flush(&mut parts, &mut elems_for_parts); + } + } + } else { + parts.push(elem.raw().to_smolstr()); + elems_for_parts.push(elem); + } + } else { + flush(&mut parts, &mut elems_for_parts); + } + } + + if !parts.is_empty() { + flush(&mut parts, &mut elems_for_parts); + } + + acc + } + ObjectReferenceKind::WildcardIdentifier => { + let mut acc = Vec::new(); + + for elem in self.0.recursive_crawl( + &["identifier", "star", "naked_identifier"], + true, + None, + true, + ) { + acc.extend(self.iter_reference_parts(elem)); + } + + acc + } + } } fn iter_reference_parts(&self, elem: ErasedSegment) -> Vec { diff --git a/crates/lib/src/rules/aliasing.rs b/crates/lib/src/rules/aliasing.rs index 53d9bb429..0efa1251a 100644 --- a/crates/lib/src/rules/aliasing.rs +++ b/crates/lib/src/rules/aliasing.rs @@ -17,7 +17,7 @@ pub fn rules() -> Vec { AL01::RuleAL01::default().erased(), AL02::RuleAL02::default().erased(), AL03::RuleAL03.erased(), - AL04::RuleAL04.erased(), + AL04::RuleAL04::default().erased(), AL05::RuleAL05.erased(), AL06::RuleAL06::default().erased(), AL07::RuleAL07::default().erased(), diff --git a/crates/lib/src/rules/aliasing/AL04.rs b/crates/lib/src/rules/aliasing/AL04.rs index 85c278acf..1f73811c6 100644 --- a/crates/lib/src/rules/aliasing/AL04.rs +++ b/crates/lib/src/rules/aliasing/AL04.rs @@ -1,19 +1,39 @@ +use std::fmt::Debug; + use ahash::{AHashMap, AHashSet}; +use smol_str::SmolStr; use crate::core::config::Value; -use crate::core::dialects::common::AliasInfo; +use crate::core::dialects::common::{AliasInfo, ColumnAliasInfo}; use crate::core::rules::base::{Erased, ErasedRule, LintResult, Rule, RuleGroups}; use crate::core::rules::context::RuleContext; use crate::core::rules::crawlers::{Crawler, SegmentSeekerCrawler}; +use crate::dialects::ansi::ObjectReferenceSegment; use crate::helpers::IndexSet; use crate::utils::analysis::select::get_select_statement_info; -#[derive(Debug, Clone, Default)] -pub struct RuleAL04; +#[derive(Debug, Clone)] +pub struct RuleAL04 { + pub(crate) lint_references_and_aliases: fn( + Vec, + Vec, + Vec, + Vec, + Vec, + &T, + ) -> Vec, + pub(crate) context: T, +} -impl Rule for RuleAL04 { +impl Default for RuleAL04 { + fn default() -> Self { + RuleAL04 { lint_references_and_aliases: Self::lint_references_and_aliases, context: () } + } +} + +impl Rule for RuleAL04 { fn load_from_config(&self, _config: &AHashMap) -> Result { - Ok(RuleAL04.erased()) + Ok(RuleAL04::default().erased()) } fn name(&self) -> &'static str { @@ -84,7 +104,14 @@ FROM let _parent_select = context.parent_stack.iter().rev().find(|seg| seg.is_type("select_statement")); - self.lint_references_and_aliases(select_info.table_aliases).unwrap_or_default() + (self.lint_references_and_aliases)( + select_info.table_aliases, + select_info.standalone_aliases, + select_info.reference_buffer, + select_info.col_aliases, + select_info.using_cols, + &self.context, + ) } fn crawl_behaviour(&self) -> Crawler { @@ -94,9 +121,13 @@ FROM impl RuleAL04 { pub fn lint_references_and_aliases( - &self, table_aliases: Vec, - ) -> Option> { + _: Vec, + _: Vec, + _: Vec, + _: Vec, + _: &(), + ) -> Vec { let mut duplicates = IndexSet::default(); let mut seen: AHashSet<_> = AHashSet::new(); @@ -106,28 +137,22 @@ impl RuleAL04 { } } - if duplicates.is_empty() { - None - } else { - Some( - duplicates - .into_iter() - .map(|alias| { - LintResult::new( - alias.segment.clone(), - Vec::new(), - None, - format!( - "Duplicate table alias '{}'. Table aliases should be unique.", - alias.ref_str - ) - .into(), - None, - ) - }) - .collect(), - ) - } + duplicates + .into_iter() + .map(|alias| { + LintResult::new( + alias.segment.clone(), + Vec::new(), + None, + format!( + "Duplicate table alias '{}'. Table aliases should be unique.", + alias.ref_str + ) + .into(), + None, + ) + }) + .collect() } } @@ -138,7 +163,7 @@ mod tests { use crate::rules::aliasing::AL04::RuleAL04; fn rules() -> Vec { - vec![RuleAL04.erased()] + vec![RuleAL04::default().erased()] } #[test] diff --git a/crates/lib/src/rules/references.rs b/crates/lib/src/rules/references.rs index 5f1bc5f9b..49a927bc4 100644 --- a/crates/lib/src/rules/references.rs +++ b/crates/lib/src/rules/references.rs @@ -1,6 +1,7 @@ use crate::core::rules::base::ErasedRule; pub mod RF01; +mod RF02; pub mod RF03; pub mod RF04; pub mod RF05; @@ -11,6 +12,7 @@ pub fn rules() -> Vec { vec![ RF01::RuleRF01.erased(), + RF02::RuleRF02::default().erased(), RF03::RuleRF03::default().erased(), RF04::RuleRF04::default().erased(), RF05::RuleRF05::default().erased(), diff --git a/crates/lib/src/rules/references/RF01.rs b/crates/lib/src/rules/references/RF01.rs index d5b245090..4b5ff547b 100644 --- a/crates/lib/src/rules/references/RF01.rs +++ b/crates/lib/src/rules/references/RF01.rs @@ -43,7 +43,8 @@ impl RuleRF01 { } if let Some(object_reference) = &alias.object_reference { - let references = ObjectReferenceSegment(object_reference.clone()) + let references = object_reference + .reference() .iter_raw_references() .into_iter() .map(|it| it.part.into()) @@ -217,7 +218,7 @@ FROM foo let dml_target_table = if !context.segment.is_type("select_statement") { let refs = context.segment.recursive_crawl(&["table_reference"], true, None, true); if let Some(reference) = refs.first() { - let reference = ObjectReferenceSegment(reference.clone()); + let reference = reference.reference(); tmp = reference .iter_raw_references() diff --git a/crates/lib/src/rules/references/RF02.rs b/crates/lib/src/rules/references/RF02.rs new file mode 100644 index 000000000..d620e490f --- /dev/null +++ b/crates/lib/src/rules/references/RF02.rs @@ -0,0 +1,163 @@ +use ahash::AHashMap; +use itertools::Itertools; +use regex::Regex; +use smol_str::SmolStr; + +use crate::core::config::Value; +use crate::core::dialects::common::{AliasInfo, ColumnAliasInfo}; +use crate::core::rules::base::{CloneRule, ErasedRule, LintResult, Rule, RuleGroups}; +use crate::core::rules::context::RuleContext; +use crate::core::rules::crawlers::{Crawler, SegmentSeekerCrawler}; +use crate::dialects::ansi::ObjectReferenceSegment; +use crate::rules::aliasing::AL04::RuleAL04; + +#[derive(Clone, Debug)] +pub struct RuleRF02 { + base: RuleAL04<(Vec, Vec)>, +} + +impl Default for RuleRF02 { + fn default() -> Self { + Self { + base: RuleAL04 { + lint_references_and_aliases: Self::lint_references_and_aliases, + context: (Vec::new(), Vec::new()), + }, + } + } +} + +impl Rule for RuleRF02 { + fn load_from_config(&self, config: &AHashMap) -> Result { + 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(); + + Ok(Self { + base: RuleAL04 { + lint_references_and_aliases: Self::lint_references_and_aliases, + context: (ignore_words, ignore_words_regex), + }, + } + .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 { + self.base.eval(context) + } + + fn crawl_behaviour(&self) -> Crawler { + SegmentSeekerCrawler::new(["select_statement"].into()).into() + } +} + +impl RuleRF02 { + fn lint_references_and_aliases( + table_aliases: Vec, + standalone_aliases: Vec, + references: Vec, + col_aliases: Vec, + using_cols: Vec, + context: &(Vec, Vec), + ) -> Vec { + if table_aliases.len() <= 1 { + return Vec::new(); + } + + let mut violation_buff = Vec::new(); + for r in references { + if context.0.contains(&r.0.raw().to_lowercase()) { + continue; + } + + if context.1.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().into()) + && !standalone_aliases.contains(&r.0.raw().into()) + { + violation_buff.push(LintResult::new( + r.0.clone().into(), + Vec::new(), + None, + format!( + "Unqualified reference {} found in select with more than one referenced \ + table/view.", + r.0.raw() + ) + .into(), + None, + )); + } + } + + violation_buff + } +} diff --git a/crates/lib/test/fixtures/rules/std_rule_cases/RF02.yml b/crates/lib/test/fixtures/rules/std_rule_cases/RF02.yml new file mode 100644 index 000000000..28851585b --- /dev/null +++ b/crates/lib/test/fixtures/rules/std_rule_cases/RF02.yml @@ -0,0 +1,426 @@ +rule: RF02 + +test_pass_qualified_references_multi_table_statements: + pass_str: | + SELECT foo.a, vee.b + FROM foo + LEFT JOIN vee ON vee.a = foo.a + +test_fail_unqualified_references_multi_table_statements: + fail_str: | + SELECT a, b + FROM foo + LEFT JOIN vee ON vee.a = foo.a + +test_pass_qualified_references_multi_table_statements_subquery: + pass_str: | + SELECT a + FROM ( + SELECT foo.a, vee.b + FROM foo + LEFT JOIN vee ON vee.a = foo.a + ) + +test_fail_unqualified_references_multi_table_statements_subquery: + fail_str: | + SELECT a + FROM ( + SELECT a, b + FROM foo + LEFT JOIN vee ON vee.a = foo.a + ) + +test_pass_qualified_references_multi_table_statements_subquery_mix: + pass_str: | + SELECT foo.a, vee.b + FROM ( + SELECT c + FROM bar + ) AS foo + LEFT JOIN vee ON vee.a = foo.a + +test_allow_date_parts_as_function_parameter_bigquery: + # Allow use of BigQuery date parts (which are not quoted and were previously + # mistaken for column references and flagged by this rule). + pass_str: | + SELECT timestamp_trunc(a.ts, month) AS t + FROM a + JOIN b ON a.id = b.id + configs: + core: + dialect: bigquery + +test_allow_date_parts_as_function_parameter_snowflake: + # Allow use of Snowflake date parts (which are not quoted and were previously + # mistaken for column references and flagged by this rule). + pass_str: | + SELECT datediff(year, a.column1, b.column2) + FROM a + JOIN b ON a.id = b.id + configs: + core: + dialect: snowflake + +test_ignore_value_table_functions_when_counting_tables: + # Allow use of unnested value tables from bigquery without counting as a + # table reference. This test passes despite unqualified reference + # because we "only select from one table" + pass_str: | + select + unqualified_reference_from_table_a, + _t_start + from a + left join unnest(generate_timestamp_array( + '2020-01-01', '2020-01-30', interval 1 day)) as _t_start + on true + configs: + core: + dialect: bigquery + +test_ignore_value_table_functions_when_counting_unqualified_aliases: + # Allow use of unnested value tables from bigquery without qualification. + # The function `unnest` returns a table which is only one unnamed column. + # This is impossible to qualify further, and as such the rule allows it. + pass_str: | + select + a.*, + b.*, + _t_start + from a + left join b + on true + left join unnest(generate_timestamp_array( + '2020-01-01', '2020-01-30', interval 1 day)) as _t_start + on true + configs: + core: + dialect: bigquery + +test_allow_unqualified_references_in_sparksql_lambdas: + pass_str: | + SELECT transform(array(1, 2, 3), x -> x + 1); + configs: + core: + dialect: sparksql + +test_allow_unqualified_references_in_athena_lambdas: + pass_str: | + select + t1.id, + filter(array[t1.col1, t1.col2, t2.col3], x -> x is not null) as flt + from t1 + inner join t2 on t1.id = t2.id + configs: + core: + dialect: athena + +test_allow_unqualified_references_in_athena_lambdas_with_several_arguments: + pass_str: | + select + t1.id, + filter(array[(t1.col1, t1.col2)], (x, y) -> x + y) as flt + from t1 + inner join t2 on t1.id = t2.id + configs: + core: + dialect: athena + +test_disallow_unqualified_references_in_malformed_lambdas: + fail_str: | + select + t1.id, + filter(array[(t1.col1, t1.col2)], (x, y), z -> x + y) as flt + from t1 + inner join t2 on t1.id = t2.id + configs: + core: + dialect: athena + +test_fail_column_and_alias_same_name: + # See issue #2169 + fail_str: | + SELECT + foo AS foo, + bar AS bar + FROM + a LEFT JOIN b ON a.id = b.id + +test_pass_column_and_alias_same_name_1: + pass_str: | + SELECT + a.foo AS foo, + b.bar AS bar + FROM + a LEFT JOIN b ON a.id = b.id + +test_pass_column_and_alias_same_name_2: + # Possible for unqualified columns if + # it is actually an alias of another column. + pass_str: | + SELECT + a.bar AS baz, + baz + FROM + a LEFT JOIN b ON a.id = b.id + +test_pass_qualified_references_multi_table_statements_mysql: + pass_str: | + SELECT foo.a, vee.b + FROM foo + LEFT JOIN vee ON vee.a = foo.a + configs: + core: + dialect: mysql + +test_fail_unqualified_references_multi_table_statements_mysql: + fail_str: | + SELECT a, b + FROM foo + LEFT JOIN vee ON vee.a = foo.a + configs: + core: + dialect: mysql + +test_fail_column_and_alias_same_name_mysql: + # See issue #2169 + fail_str: | + SELECT + foo AS foo, + bar AS bar + FROM + a LEFT JOIN b ON a.id = b.id + configs: + core: + dialect: mysql + +test_pass_column_and_alias_same_name_1_mysql: + pass_str: | + SELECT + a.foo AS foo, + b.bar AS bar + FROM + a LEFT JOIN b ON a.id = b.id + configs: + core: + dialect: mysql + +test_pass_column_and_alias_same_name_2_mysql: + # Possible for unqualified columns if + # it is actually an alias of another column. + pass_str: | + SELECT + a.bar AS baz, + baz + FROM + a LEFT JOIN b ON a.id = b.id + configs: + core: + dialect: mysql + +test_pass_variable_reference_in_where_clause_mysql: + pass_str: | + SET @someVar = 1; + SELECT + Table1.Col1, + Table2.Col2 + FROM Table1 + LEFT JOIN Table2 ON Table1.Join1 = Table2.Join1 + WHERE Table1.FilterCol = @someVar; + configs: + core: + dialect: mysql + +test_pass_qualified_references_multi_table_statements_tsql: + pass_str: | + SELECT foo.a, vee.b + FROM foo + LEFT JOIN vee ON vee.a = foo.a + configs: + core: + dialect: tsql + +test_fail_unqualified_references_multi_table_statements_tsql: + fail_str: | + SELECT a, b + FROM foo + LEFT JOIN vee ON vee.a = foo.a + configs: + core: + dialect: tsql + +test_fail_column_and_alias_same_name_tsql: + # See issue #2169 + fail_str: | + SELECT + foo AS foo, + bar AS bar + FROM + a LEFT JOIN b ON a.id = b.id + configs: + core: + dialect: tsql + +test_pass_column_and_alias_same_name_1_tsql: + pass_str: | + SELECT + a.foo AS foo, + b.bar AS bar + FROM + a LEFT JOIN b ON a.id = b.id + configs: + core: + dialect: tsql + +test_pass_column_and_alias_same_name_2_tsql: + # Possible for unqualified columns if + # it is actually an alias of another column. + pass_str: | + SELECT + a.bar AS baz, + baz + FROM + a LEFT JOIN b ON a.id = b.id + configs: + core: + dialect: tsql + +test_pass_rowtype_with_join: + # Check we don't wrongly interpret rowtype attributes + # as field alias when more than one tables in join + pass_str: | + select + cast(row(t1.attr, t2.attr) as row(fld1 double, fld2 double)) as flds + from sch.tab1 as t1 + join sch.tab2 as t2 on t2.id = t1.id + configs: + core: + dialect: hive + +test_fail_table_plus_flatten_snowflake_1: + # FLATTEN() returns a table, thus there are two tables, thus lint failure. + fail_str: | + SELECT + r.rec:foo::string AS foo, + value:bar::string AS bar + FROM foo.bar AS r, LATERAL FLATTEN(input => r.rec:result) AS x + configs: + core: + dialect: snowflake + +test_fail_table_plus_flatten_snowflake_2: + # FLATTEN() returns a table, thus there are two tables, thus lint failure, + # even though there's no alias provided for FLATTEN(). + fail_str: | + SELECT + r.rec:foo::string AS foo, + value:bar::string AS bar + FROM foo.bar AS r, LATERAL FLATTEN(input => r.rec:result) + configs: + core: + dialect: snowflake + +test_pass_table_plus_flatten_snowflake_1: + # FLATTEN() returns a table, thus there are two tables. This one passes, + # unlike the above, because both aliases are used. + pass_str: | + SELECT + r.rec:foo::string AS foo, + x.value:bar::string AS bar + FROM foo.bar AS r, LATERAL FLATTEN(input => r.rec:result) AS x + configs: + core: + dialect: snowflake + +test_pass_ignore_words_column_name: + pass_str: | + SELECT test1, test2 + FROM t_table1 + LEFT JOIN t_table_2 + ON TRUE + configs: + rules: + references.qualification: + ignore_words: test1,test2 + +test_pass_ignore_words_regex_column_name: + pass_str: | + SELECT _test1, _test2 + FROM t_table1 + LEFT JOIN t_table_2 + ON TRUE + configs: + rules: + references.qualification: + ignore_words_regex: ^_ + +test_pass_ignore_words_regex_bigquery_declare_example: + pass_str: + DECLARE _test INT64 DEFAULT 42; + SELECT _test + FROM t_table1 + LEFT JOIN t_table_2 + ON TRUE + configs: + core: + dialect: bigquery + rules: + references.qualification: + ignore_words_regex: ^_ + +test_pass_redshift: + # This was failing in issue 3380. + pass_str: + SELECT account.id + FROM salesforce_sd.account + INNER JOIN salesforce_sd."user" ON salesforce_sd."user".id = account.ownerid + configs: + core: + dialect: redshift + +test_pass_tsql: + # This was failing in issue 3342. + pass_str: + select + psc.col1 + from + tbl1 as psc + where + exists + ( + select 1 as data + from + tbl2 as pr + join tbl2 as c on c.cid = pr.cid + where + c.col1 = 'x' + and pr.col2 <= convert(date, getdate()) + and pr.pid = psc.pid + ) + configs: + core: + dialect: tsql + +test_pass_ansi: + # This was failing in issue 3055. + pass_str: | + SELECT my_col + FROM my_table + WHERE EXISTS ( + SELECT 1 + FROM other_table + INNER JOIN mapping_table ON (mapping_table.other_fk = other_table.id_pk) + WHERE mapping_table.kind = my_table.kind + ) + +test_pass_redshift_convert: + # This was failing in issue 3651. + pass_str: | + SELECT + sellers.name, + CONVERT(integer, sales.pricepaid) AS price + FROM sales + LEFT JOIN sellers ON sales.sellerid = sellers.sellerid + WHERE sales.salesid = 100 + configs: + core: + dialect: redshift