use flowscope_core::{
analyze, issue_codes, AnalysisOptions, AnalyzeRequest, Dialect, LintConfig, Severity,
};
#[cfg(feature = "templating")]
use flowscope_core::{TemplateConfig, TemplateMode};
fn run_lint(sql: &str) -> Vec<(String, String)> {
run_lint_in_dialect(sql, Dialect::Generic)
}
fn run_lint_in_dialect(sql: &str, dialect: Dialect) -> Vec<(String, String)> {
let result = analyze(&AnalyzeRequest {
sql: sql.to_string(),
files: None,
dialect,
source_name: None,
options: Some(AnalysisOptions {
lint: Some(LintConfig::default()),
..Default::default()
}),
schema: None,
#[cfg(feature = "templating")]
template_config: None,
});
result
.issues
.iter()
.filter(|i| i.code.starts_with("LINT_"))
.map(|i| (i.code.clone(), i.message.clone()))
.collect()
}
fn run_lint_with_config(sql: &str, config: LintConfig) -> Vec<(String, String)> {
run_lint_with_config_in_dialect(sql, Dialect::Generic, config)
}
fn run_lint_with_config_in_dialect(
sql: &str,
dialect: Dialect,
config: LintConfig,
) -> Vec<(String, String)> {
let result = analyze(&AnalyzeRequest {
sql: sql.to_string(),
files: None,
dialect,
source_name: None,
options: Some(AnalysisOptions {
lint: Some(config),
..Default::default()
}),
schema: None,
#[cfg(feature = "templating")]
template_config: None,
});
result
.issues
.iter()
.filter(|i| i.code.starts_with("LINT_"))
.map(|i| (i.code.clone(), i.message.clone()))
.collect()
}
#[cfg(feature = "templating")]
fn run_lint_in_dialect_with_jinja_template(sql: &str, dialect: Dialect) -> Vec<(String, String)> {
run_lint_with_config_in_dialect_with_jinja_template(sql, dialect, LintConfig::default())
}
#[cfg(feature = "templating")]
fn run_lint_with_config_in_dialect_with_jinja_template(
sql: &str,
dialect: Dialect,
config: LintConfig,
) -> Vec<(String, String)> {
let result = analyze(&AnalyzeRequest {
sql: sql.to_string(),
files: None,
dialect,
source_name: None,
options: Some(AnalysisOptions {
lint: Some(config),
..Default::default()
}),
schema: None,
template_config: Some(TemplateConfig {
mode: TemplateMode::Jinja,
context: std::collections::HashMap::new(),
}),
});
result
.issues
.iter()
.filter(|i| i.code.starts_with("LINT_"))
.map(|i| (i.code.clone(), i.message.clone()))
.collect()
}
#[test]
fn lint_issues_appear_in_analyze_result() {
let result = analyze(&AnalyzeRequest {
sql: "SELECT 1 UNION SELECT 2".to_string(),
files: None,
dialect: Dialect::Generic,
source_name: None,
options: Some(AnalysisOptions {
lint: Some(LintConfig::default()),
..Default::default()
}),
schema: None,
#[cfg(feature = "templating")]
template_config: None,
});
let lint_issues: Vec<_> = result
.issues
.iter()
.filter(|i| i.code.starts_with("LINT_"))
.collect();
assert!(!lint_issues.is_empty(), "expected lint issues in result");
let am002 = lint_issues
.iter()
.find(|issue| issue.code == issue_codes::LINT_AM_002)
.expect("expected AM002 lint issue in result");
assert_eq!(am002.severity, Severity::Warning);
}
#[test]
fn lint_issues_have_statement_index() {
let result = analyze(&AnalyzeRequest {
sql: "SELECT 1; SELECT 1 UNION SELECT 2".to_string(),
files: None,
dialect: Dialect::Generic,
source_name: None,
options: Some(AnalysisOptions {
lint: Some(LintConfig::default()),
..Default::default()
}),
schema: None,
#[cfg(feature = "templating")]
template_config: None,
});
let lint_issues: Vec<_> = result
.issues
.iter()
.filter(|i| i.code == issue_codes::LINT_AM_002)
.collect();
assert_eq!(lint_issues.len(), 1);
assert_eq!(
lint_issues[0].statement_index,
Some(1),
"lint issue should reference the second statement"
);
}
#[test]
fn lint_disabled_rule_not_reported() {
let issues = run_lint_with_config(
"SELECT 1\nUNION\nSELECT 2\n",
LintConfig {
enabled: true,
disabled_rules: vec!["LINT_AM_002".to_string()],
rule_configs: std::collections::BTreeMap::new(),
},
);
assert!(
!issues
.iter()
.any(|(code, _)| code == issue_codes::LINT_AM_002),
"disabled rule should not be reported: {issues:?}"
);
}
#[test]
fn lint_disabled_globally() {
let issues = run_lint_with_config(
"SELECT 1 UNION SELECT 2",
LintConfig {
enabled: false,
disabled_rules: vec![],
rule_configs: std::collections::BTreeMap::new(),
},
);
assert!(
issues.is_empty(),
"globally disabled linter should produce no issues"
);
}
#[test]
fn lint_rule_config_aliasing_table_implicit_policy() {
let issues = run_lint_with_config(
"SELECT * FROM users AS u",
LintConfig {
enabled: true,
disabled_rules: vec![],
rule_configs: std::collections::BTreeMap::from([(
"aliasing.table".to_string(),
serde_json::json!({"aliasing": "implicit"}),
)]),
},
);
assert!(
issues.iter().any(|(code, _)| code == "LINT_AL_001"),
"implicit aliasing policy should flag explicit AS: {issues:?}"
);
}
#[test]
fn lint_rule_config_aliasing_column_implicit_policy() {
let issues = run_lint_with_config(
"SELECT a + 1 AS value FROM t",
LintConfig {
enabled: true,
disabled_rules: vec![],
rule_configs: std::collections::BTreeMap::from([(
"LINT_AL_002".to_string(),
serde_json::json!({"aliasing": "implicit"}),
)]),
},
);
assert!(
issues.iter().any(|(code, _)| code == "LINT_AL_002"),
"implicit aliasing policy should flag explicit AS: {issues:?}"
);
}
#[test]
fn lint_rule_config_aliasing_length_max() {
let issues = run_lint_with_config(
"SELECT * FROM users eleven_chars",
LintConfig {
enabled: true,
disabled_rules: vec![],
rule_configs: std::collections::BTreeMap::from([(
"LINT_AL_006".to_string(),
serde_json::json!({"max_alias_length": 10}),
)]),
},
);
assert!(
issues.iter().any(|(code, _)| code == "LINT_AL_006"),
"configured max alias length should flag long alias: {issues:?}"
);
}
#[test]
fn lint_rule_config_aliasing_expression_allow_scalar_false() {
let issues = run_lint_with_config(
"SELECT 1 FROM t",
LintConfig {
enabled: true,
disabled_rules: vec![],
rule_configs: std::collections::BTreeMap::from([(
"LINT_AL_003".to_string(),
serde_json::json!({"allow_scalar": false}),
)]),
},
);
assert!(
issues.iter().any(|(code, _)| code == "LINT_AL_003"),
"allow_scalar=false should flag scalar expression aliases: {issues:?}"
);
}
#[test]
fn lint_rule_config_not_equal_style_c_style() {
let issues = run_lint_with_config(
"SELECT * FROM t WHERE a <> b",
LintConfig {
enabled: true,
disabled_rules: vec![],
rule_configs: std::collections::BTreeMap::from([(
"LINT_CV_001".to_string(),
serde_json::json!({"preferred_not_equal_style": "c_style"}),
)]),
},
);
assert!(
issues.iter().any(|(code, _)| code == "LINT_CV_001"),
"c_style not-equal preference should flag <> usage: {issues:?}"
);
}
#[test]
fn lint_rule_config_count_rows_prefer_count_one() {
let issues = run_lint_with_config(
"SELECT COUNT(*) FROM t",
LintConfig {
enabled: true,
disabled_rules: vec![],
rule_configs: std::collections::BTreeMap::from([(
"convention.count_rows".to_string(),
serde_json::json!({"prefer_count_1": true}),
)]),
},
);
assert!(
issues.iter().any(|(code, _)| code == "LINT_CV_004"),
"prefer_count_1 should flag COUNT(*): {issues:?}"
);
}
#[test]
fn lint_rule_config_terminator_require_final_semicolon() {
let issues = run_lint_with_config(
"SELECT 1",
LintConfig {
enabled: true,
disabled_rules: vec![],
rule_configs: std::collections::BTreeMap::from([(
"convention.terminator".to_string(),
serde_json::json!({"require_final_semicolon": true}),
)]),
},
);
assert!(
issues.iter().any(|(code, _)| code == "LINT_CV_006"),
"require_final_semicolon should flag missing final semicolon: {issues:?}"
);
}
#[test]
fn lint_rule_config_terminator_require_final_semicolon_mssql_go_batches() {
let result = analyze(&AnalyzeRequest {
sql: "CREATE SCHEMA staging;\nGO\nCREATE TABLE test (id INT)\n".to_string(),
files: None,
dialect: Dialect::Mssql,
source_name: None,
options: Some(AnalysisOptions {
lint: Some(LintConfig {
enabled: true,
disabled_rules: vec![],
rule_configs: std::collections::BTreeMap::from([(
"convention.terminator".to_string(),
serde_json::json!({"require_final_semicolon": true}),
)]),
}),
..Default::default()
}),
schema: None,
#[cfg(feature = "templating")]
template_config: None,
});
assert!(
!result
.issues
.iter()
.any(|issue| issue.code == issue_codes::PARSE_ERROR),
"MSSQL GO batches should not fail parsing in lint mode: {:?}",
result.issues
);
assert!(
result
.issues
.iter()
.any(|issue| issue.code == issue_codes::LINT_CV_006),
"require_final_semicolon should flag last statement after GO batch separator: {:?}",
result.issues
);
}
#[test]
fn lint_rule_config_select_trailing_comma_require() {
let issues = run_lint_with_config(
"SELECT a FROM t",
LintConfig {
enabled: true,
disabled_rules: vec![],
rule_configs: std::collections::BTreeMap::from([(
"convention.select_trailing_comma".to_string(),
serde_json::json!({"select_clause_trailing_comma": "require"}),
)]),
},
);
assert!(
issues.iter().any(|(code, _)| code == "LINT_CV_003"),
"require trailing comma policy should flag missing trailing comma: {issues:?}"
);
}
#[test]
fn lint_rule_config_blocked_words_custom_list() {
let issues = run_lint_with_config(
"SELECT wip FROM t",
LintConfig {
enabled: true,
disabled_rules: vec![],
rule_configs: std::collections::BTreeMap::from([(
"convention.blocked_words".to_string(),
serde_json::json!({"blocked_words": ["wip"]}),
)]),
},
);
assert!(
issues.iter().any(|(code, _)| code == "LINT_CV_009"),
"blocked_words custom list should flag configured terms: {issues:?}"
);
}
#[test]
fn lint_rule_config_quoted_literals_double_quotes_preference() {
let issues = run_lint_with_config_in_dialect(
"SELECT 'abc' FROM t",
Dialect::Bigquery,
LintConfig {
enabled: true,
disabled_rules: vec![],
rule_configs: std::collections::BTreeMap::from([(
"LINT_CV_010".to_string(),
serde_json::json!({"preferred_quoted_literal_style": "double_quotes"}),
)]),
},
);
assert!(
issues.iter().any(|(code, _)| code == "LINT_CV_010"),
"double_quotes preference should flag single-quoted literals: {issues:?}"
);
}
#[test]
fn lint_rule_config_casting_style_shorthand() {
let issues = run_lint_with_config(
"SELECT CAST(a AS INT) FROM t",
LintConfig {
enabled: true,
disabled_rules: vec![],
rule_configs: std::collections::BTreeMap::from([(
"convention.casting_style".to_string(),
serde_json::json!({"preferred_type_casting_style": "shorthand"}),
)]),
},
);
assert!(
issues.iter().any(|(code, _)| code == "LINT_CV_011"),
"shorthand casting style should flag CAST(...) usage: {issues:?}"
);
}
#[test]
fn lint_rule_config_long_lines_max_line_length() {
let issues = run_lint_with_config(
"SELECT this_is_far_too_long FROM t",
LintConfig {
enabled: true,
disabled_rules: vec![],
rule_configs: std::collections::BTreeMap::from([(
"layout.long_lines".to_string(),
serde_json::json!({"max_line_length": 20}),
)]),
},
);
assert!(
issues.iter().any(|(code, _)| code == "LINT_LT_005"),
"configured max_line_length should flag long lines: {issues:?}"
);
}
#[test]
fn lint_rule_config_long_lines_ignore_comment_lines() {
let sql = format!("SELECT 1;\n-- {}\nSELECT 2", "x".repeat(120));
let issues = run_lint_with_config(
&sql,
LintConfig {
enabled: true,
disabled_rules: vec![],
rule_configs: std::collections::BTreeMap::from([(
"layout.long_lines".to_string(),
serde_json::json!({
"max_line_length": 20,
"ignore_comment_lines": true
}),
)]),
},
);
assert!(
!issues.iter().any(|(code, _)| code == "LINT_LT_005"),
"ignore_comment_lines=true should suppress LT_005 on comment-only lines: {issues:?}"
);
}
#[test]
fn lint_rule_config_long_lines_ignore_comment_clauses() {
let sql = format!("SELECT 1 -- {}", "x".repeat(120));
let issues = run_lint_with_config(
&sql,
LintConfig {
enabled: true,
disabled_rules: vec![],
rule_configs: std::collections::BTreeMap::from([(
"layout.long_lines".to_string(),
serde_json::json!({
"max_line_length": 20,
"ignore_comment_clauses": true
}),
)]),
},
);
assert!(
!issues.iter().any(|(code, _)| code == "LINT_LT_005"),
"ignore_comment_clauses=true should suppress LT_005 on trailing comments: {issues:?}"
);
}
#[test]
fn lint_rule_config_layout_newlines_inside_limit() {
let issues = run_lint_with_config(
"SELECT 1\n\n\nFROM t",
LintConfig {
enabled: true,
disabled_rules: vec![],
rule_configs: std::collections::BTreeMap::from([(
"layout.newlines".to_string(),
serde_json::json!({"maximum_empty_lines_inside_statements": 2}),
)]),
},
);
assert!(
!issues.iter().any(|(code, _)| code == "LINT_LT_015"),
"inside limit=2 should allow two blank lines inside statements: {issues:?}"
);
}
#[test]
fn lint_rule_config_layout_newlines_between_limit() {
let issues = run_lint_with_config(
"SELECT 1;\n\n\nSELECT 2",
LintConfig {
enabled: true,
disabled_rules: vec![],
rule_configs: std::collections::BTreeMap::from([(
"LINT_LT_015".to_string(),
serde_json::json!({"maximum_empty_lines_between_statements": 1}),
)]),
},
);
assert!(
issues.iter().any(|(code, _)| code == "LINT_LT_015"),
"between limit=1 should flag larger inter-statement gaps: {issues:?}"
);
}
#[test]
fn lint_rule_config_layout_set_operators_leading_position() {
let issues = run_lint_with_config(
"SELECT 1\nUNION SELECT 2\nUNION SELECT 3",
LintConfig {
enabled: true,
disabled_rules: vec![],
rule_configs: std::collections::BTreeMap::from([(
"layout.set_operators".to_string(),
serde_json::json!({"line_position": "leading"}),
)]),
},
);
assert!(
!issues.iter().any(|(code, _)| code == "LINT_LT_011"),
"line_position=leading should allow leading set operators: {issues:?}"
);
}
#[test]
fn lint_rule_config_layout_set_operators_trailing_position() {
let issues = run_lint_with_config(
"SELECT 1\nUNION SELECT 2",
LintConfig {
enabled: true,
disabled_rules: vec![],
rule_configs: std::collections::BTreeMap::from([(
"LINT_LT_011".to_string(),
serde_json::json!({"line_position": "trailing"}),
)]),
},
);
assert!(
issues.iter().any(|(code, _)| code == "LINT_LT_011"),
"line_position=trailing should flag leading set operators: {issues:?}"
);
}
#[test]
fn lint_rule_config_select_targets_wildcard_policy_multiple() {
let issues = run_lint_with_config(
"SELECT * FROM t",
LintConfig {
enabled: true,
disabled_rules: vec![],
rule_configs: std::collections::BTreeMap::from([(
"LINT_LT_009".to_string(),
serde_json::json!({"wildcard_policy": "multiple"}),
)]),
},
);
assert!(
issues.iter().any(|(code, _)| code == "LINT_LT_009"),
"wildcard_policy=multiple should flag single-line wildcard target: {issues:?}"
);
}
#[test]
fn lint_rule_config_layout_operators_trailing_line_position() {
let issues = run_lint_with_config(
"SELECT a\n + b FROM t",
LintConfig {
enabled: true,
disabled_rules: vec![],
rule_configs: std::collections::BTreeMap::from([(
"layout.operators".to_string(),
serde_json::json!({"line_position": "trailing"}),
)]),
},
);
assert!(
issues.iter().any(|(code, _)| code == "LINT_LT_003"),
"line_position=trailing should flag leading operators: {issues:?}"
);
}
#[test]
fn lint_rule_config_layout_commas_leading_line_position() {
let issues = run_lint_with_config(
"SELECT a,\n b FROM t",
LintConfig {
enabled: true,
disabled_rules: vec![],
rule_configs: std::collections::BTreeMap::from([(
"layout.commas".to_string(),
serde_json::json!({"line_position": "leading"}),
)]),
},
);
assert!(
issues.iter().any(|(code, _)| code == "LINT_LT_004"),
"line_position=leading should flag trailing commas: {issues:?}"
);
}
#[test]
fn lint_rule_config_layout_indent_custom_unit() {
let issues = run_lint_with_config(
"SELECT a\n , b\nFROM t",
LintConfig {
enabled: true,
disabled_rules: vec![],
rule_configs: std::collections::BTreeMap::from([(
"layout.indent".to_string(),
serde_json::json!({"indent_unit": 4}),
)]),
},
);
assert!(
issues.iter().any(|(code, _)| code == "LINT_LT_002"),
"indent_unit=4 should flag two-space indentation: {issues:?}"
);
}
#[test]
fn lint_rule_config_structure_subquery_forbid_join_only() {
let issues = run_lint_with_config(
"SELECT * FROM (SELECT * FROM t) sub",
LintConfig {
enabled: true,
disabled_rules: vec![],
rule_configs: std::collections::BTreeMap::from([(
"structure.subquery".to_string(),
serde_json::json!({"forbid_subquery_in": "join"}),
)]),
},
);
assert!(
!issues.iter().any(|(code, _)| code == "LINT_ST_005"),
"forbid_subquery_in=join should not flag FROM subqueries: {issues:?}"
);
}
#[test]
fn lint_st_005_allows_correlated_join_subquery_with_outer_alias_reference() {
let issues = run_lint(
"SELECT pd.* \
FROM person_dates AS pd \
JOIN (SELECT * FROM events AS ce WHERE ce.name = pd.name)",
);
assert!(
!issues
.iter()
.any(|(code, _)| code == issue_codes::LINT_ST_005),
"correlated JOIN subqueries should not trigger ST_005: {issues:?}"
);
}
#[test]
fn lint_st_005_allows_correlated_join_subquery_with_outer_table_name_reference() {
let issues = run_lint(
"SELECT pd.* \
FROM person_dates AS pd \
JOIN (SELECT * FROM events AS ce WHERE ce.name = person_dates.name)",
);
assert!(
!issues.iter().any(|(code, _)| code == issue_codes::LINT_ST_005),
"correlated JOIN subqueries referencing outer table names should not trigger ST_005: {issues:?}"
);
}
#[test]
fn lint_rule_config_join_condition_order_later_preference() {
let issues = run_lint_with_config(
"SELECT * FROM foo JOIN bar ON foo.id = bar.id",
LintConfig {
enabled: true,
disabled_rules: vec![],
rule_configs: std::collections::BTreeMap::from([(
"LINT_ST_009".to_string(),
serde_json::json!({"preferred_first_table_in_join_clause": "later"}),
)]),
},
);
assert!(
issues.iter().any(|(code, _)| code == "LINT_ST_009"),
"later join condition preference should flag earlier-first ordering: {issues:?}"
);
}
#[test]
fn lint_rule_config_references_from_force_enable_false() {
let issues = run_lint_with_config(
"SELECT * FROM my_tbl WHERE foo.bar > 0",
LintConfig {
enabled: true,
disabled_rules: vec![],
rule_configs: std::collections::BTreeMap::from([(
"references.from".to_string(),
serde_json::json!({"force_enable": false}),
)]),
},
);
assert!(
!issues.iter().any(|(code, _)| code == "LINT_RF_001"),
"force_enable=false should disable RF_001 checks: {issues:?}"
);
}
#[test]
fn lint_rule_config_references_qualification_force_enable_false() {
let issues = run_lint_with_config(
"SELECT id FROM a JOIN b ON a.id = b.id",
LintConfig {
enabled: true,
disabled_rules: vec![],
rule_configs: std::collections::BTreeMap::from([(
"references.qualification".to_string(),
serde_json::json!({"force_enable": false}),
)]),
},
);
assert!(
!issues.iter().any(|(code, _)| code == "LINT_RF_002"),
"force_enable=false should disable RF_002 checks: {issues:?}"
);
}
#[test]
fn lint_rule_config_references_consistent_qualified_mode() {
let issues = run_lint_with_config(
"SELECT bar FROM my_tbl",
LintConfig {
enabled: true,
disabled_rules: vec![],
rule_configs: std::collections::BTreeMap::from([(
"references.consistent".to_string(),
serde_json::json!({"single_table_references": "qualified"}),
)]),
},
);
assert!(
issues.iter().any(|(code, _)| code == "LINT_RF_003"),
"single_table_references=qualified should flag unqualified refs: {issues:?}"
);
}
#[test]
fn lint_rule_config_references_quoting_prefer_quoted_identifiers() {
let issues = run_lint_with_config(
"SELECT \"good_name\" FROM \"t\"",
LintConfig {
enabled: true,
disabled_rules: vec![],
rule_configs: std::collections::BTreeMap::from([(
"LINT_RF_006".to_string(),
serde_json::json!({"prefer_quoted_identifiers": true}),
)]),
},
);
assert!(
!issues.iter().any(|(code, _)| code == "LINT_RF_006"),
"prefer_quoted_identifiers=true should suppress unnecessary-quote warnings: {issues:?}"
);
}
#[test]
fn lint_rule_config_references_quoting_prefer_quoted_identifiers_flags_unquoted() {
let issues = run_lint_with_config(
"SELECT good_name FROM t",
LintConfig {
enabled: true,
disabled_rules: vec![],
rule_configs: std::collections::BTreeMap::from([(
"LINT_RF_006".to_string(),
serde_json::json!({"prefer_quoted_identifiers": true}),
)]),
},
);
assert!(
issues.iter().any(|(code, _)| code == "LINT_RF_006"),
"prefer_quoted_identifiers=true should flag unquoted identifiers: {issues:?}"
);
}
#[test]
fn lint_rule_config_references_keywords_quoted_policy_all() {
let issues = run_lint_with_config(
"SELECT \"select\".id FROM users AS \"select\"",
LintConfig {
enabled: true,
disabled_rules: vec![],
rule_configs: std::collections::BTreeMap::from([(
"references.keywords".to_string(),
serde_json::json!({"quoted_identifiers_policy": "all"}),
)]),
},
);
assert!(
issues.iter().any(|(code, _)| code == "LINT_RF_004"),
"quoted_identifiers_policy=all should flag quoted keyword identifiers: {issues:?}"
);
}
#[test]
fn lint_rule_config_references_special_chars_additional_allowed() {
let issues = run_lint_with_config(
"SELECT \"bad-name\" FROM t",
LintConfig {
enabled: true,
disabled_rules: vec![],
rule_configs: std::collections::BTreeMap::from([(
"references.special_chars".to_string(),
serde_json::json!({"additional_allowed_characters": "-"}),
)]),
},
);
assert!(
!issues.iter().any(|(code, _)| code == "LINT_RF_005"),
"additional_allowed_characters should suppress RF_005 for configured chars: {issues:?}"
);
}
#[test]
fn lint_rule_config_aliasing_forbid_force_enable_false() {
let issues = run_lint_with_config(
"SELECT * FROM users u JOIN orders o ON u.id = o.user_id",
LintConfig {
enabled: true,
disabled_rules: vec![],
rule_configs: std::collections::BTreeMap::from([(
"aliasing.forbid".to_string(),
serde_json::json!({"force_enable": false}),
)]),
},
);
assert!(
!issues.iter().any(|(code, _)| code == "LINT_AL_007"),
"force_enable=false should disable AL_007 checks: {issues:?}"
);
}
#[test]
fn lint_al_007_disabled_by_default() {
let issues = run_lint("SELECT * FROM users u JOIN orders o ON u.id = o.user_id");
assert!(
!issues.iter().any(|(code, _)| code == "LINT_AL_007"),
"AL_007 should be disabled by default unless force_enable=true: {issues:?}"
);
}
#[test]
fn lint_al_007_flags_unnecessary_aliases_in_multi_source_non_self_join_query() {
let issues = run_lint_with_config(
"SELECT * FROM users u JOIN orders o ON u.id = o.user_id",
LintConfig {
enabled: true,
disabled_rules: vec![],
rule_configs: std::collections::BTreeMap::from([(
"aliasing.forbid".to_string(),
serde_json::json!({"force_enable": true}),
)]),
},
);
let al07_count = issues
.iter()
.filter(|(code, _)| code == "LINT_AL_007")
.count();
assert_eq!(
al07_count, 2,
"both non-self-join table aliases should trigger AL_007: {issues:?}"
);
}
#[test]
fn lint_al_007_allows_self_join_aliases_but_flags_extra_unique_alias() {
let issues = run_lint_with_config(
"SELECT * FROM users u1 JOIN users u2 ON u1.id = u2.id JOIN orders o ON o.user_id = u1.id",
LintConfig {
enabled: true,
disabled_rules: vec![],
rule_configs: std::collections::BTreeMap::from([(
"aliasing.forbid".to_string(),
serde_json::json!({"force_enable": true}),
)]),
},
);
let al07_count = issues
.iter()
.filter(|(code, _)| code == "LINT_AL_007")
.count();
assert_eq!(
al07_count, 1,
"self-join aliases should be allowed while unrelated unique aliases are still flagged: {issues:?}"
);
}
#[test]
fn lint_al_005_flags_unused_alias_in_single_table_query() {
let issues = run_lint("SELECT * FROM users u");
assert!(
issues
.iter()
.any(|(code, _)| code == issue_codes::LINT_AL_005),
"single-table unused aliases should trigger AL_005: {issues:?}"
);
}
#[test]
fn lint_al_005_allows_used_alias_in_single_table_query() {
let issues = run_lint("SELECT u.id FROM users u");
assert!(
!issues
.iter()
.any(|(code, _)| code == issue_codes::LINT_AL_005),
"single-table aliases referenced in expressions should not trigger AL_005: {issues:?}"
);
}
#[test]
fn lint_al_005_dialect_mode_generic_allows_quoted_alias_fold_match() {
let issues = run_lint("SELECT a.col1 FROM tab1 AS \"A\"");
assert!(
!issues
.iter()
.any(|(code, _)| code == issue_codes::LINT_AL_005),
"generic dialect should treat naked/quoted casefold matches as alias usage in AL_005: {issues:?}"
);
}
#[test]
fn lint_al_005_dialect_mode_snowflake_flags_quoted_case_mismatch() {
let issues = run_lint_in_dialect("SELECT a.col_1 FROM table_a AS \"a\"", Dialect::Snowflake);
assert!(
issues
.iter()
.any(|(code, _)| code == issue_codes::LINT_AL_005),
"Snowflake naked/quoted case mismatch should be treated as unused alias in AL_005: {issues:?}"
);
}
#[test]
fn lint_al_005_dialect_mode_redshift_allows_lower_fold_for_quoted_alias() {
let issues = run_lint_in_dialect("SELECT A.col_1 FROM table_a AS \"a\"", Dialect::Redshift);
assert!(
!issues
.iter()
.any(|(code, _)| code == issue_codes::LINT_AL_005),
"Redshift should fold naked identifiers to lower-case for quoted/naked alias matching in AL_005: {issues:?}"
);
}
#[test]
fn lint_al_005_dialect_mode_redshift_flags_mixed_quoted_case_mismatch() {
let issues = run_lint_in_dialect("SELECT a.col_1 FROM table_a AS \"A\"", Dialect::Redshift);
assert!(
issues
.iter()
.any(|(code, _)| code == issue_codes::LINT_AL_005),
"Redshift quoted/naked mismatched-case aliases should still trigger AL_005: {issues:?}"
);
}
#[test]
fn lint_al_005_dialect_mode_mysql_allows_backtick_reference_with_unquoted_alias() {
let issues = run_lint_in_dialect(
"SELECT `nih`.`userID` FROM `flight_notification_item_history` AS nih",
Dialect::Mysql,
);
assert!(
!issues
.iter()
.any(|(code, _)| code == issue_codes::LINT_AL_005),
"MySQL backtick-qualified references should count as usage for unquoted aliases in AL_005: {issues:?}"
);
}
#[test]
fn lint_al_005_dialect_mode_duckdb_allows_case_insensitive_quoted_reference() {
let issues = run_lint_in_dialect("SELECT \"a\".col_1 FROM table_a AS A", Dialect::Duckdb);
assert!(
!issues
.iter()
.any(|(code, _)| code == issue_codes::LINT_AL_005),
"DuckDB quoted identifier references should match unquoted aliases case-insensitively in AL_005: {issues:?}"
);
}
#[test]
fn lint_al_005_dialect_mode_hive_allows_case_insensitive_quoted_reference() {
let issues = run_lint_in_dialect("SELECT `a`.col1 FROM tab1 AS A", Dialect::Hive);
assert!(
!issues
.iter()
.any(|(code, _)| code == issue_codes::LINT_AL_005),
"Hive quoted identifier references should match unquoted aliases case-insensitively in AL_005: {issues:?}"
);
}
#[test]
fn lint_al_005_bigquery_escaped_quoted_identifiers_do_not_parse_error() {
let result = analyze(&AnalyzeRequest {
sql: "SELECT `\\`a`.col1\nFROM tab1 as `\\`A`".to_string(),
files: None,
dialect: Dialect::Bigquery,
source_name: None,
options: Some(AnalysisOptions {
lint: Some(LintConfig::default()),
..Default::default()
}),
schema: None,
#[cfg(feature = "templating")]
template_config: None,
});
assert!(
!result
.issues
.iter()
.any(|issue| issue.code == issue_codes::PARSE_ERROR),
"escaped BigQuery quoted identifiers should parse in fallback mode: {:?}",
result.issues
);
assert!(
!result
.issues
.iter()
.any(|issue| issue.code == issue_codes::LINT_AL_005),
"escaped BigQuery quoted identifiers should not trigger AL_005 when alias is referenced: {:?}",
result.issues
);
}
#[test]
fn lint_al_005_clickhouse_escaped_quoted_identifiers_do_not_parse_error() {
let result = analyze(&AnalyzeRequest {
sql: "SELECT \"\\\"`a`\"\"\".col1,\nFROM tab1 as `\"\\`a``\"`".to_string(),
files: None,
dialect: Dialect::Clickhouse,
source_name: None,
options: Some(AnalysisOptions {
lint: Some(LintConfig::default()),
..Default::default()
}),
schema: None,
#[cfg(feature = "templating")]
template_config: None,
});
assert!(
!result
.issues
.iter()
.any(|issue| issue.code == issue_codes::PARSE_ERROR),
"escaped ClickHouse quoted identifiers should parse in fallback mode: {:?}",
result.issues
);
assert!(
!result
.issues
.iter()
.any(|issue| issue.code == issue_codes::LINT_AL_005),
"escaped ClickHouse quoted identifiers should not trigger AL_005 when alias is referenced: {:?}",
result.issues
);
}
#[test]
fn lint_al_005_alias_used_in_qualify_clause() {
let issues = run_lint_in_dialect(
"SELECT u.id FROM users u JOIN orders o ON users.id = orders.user_id QUALIFY ROW_NUMBER() OVER (PARTITION BY o.user_id ORDER BY o.user_id) = 1",
Dialect::Snowflake,
);
assert!(
!issues
.iter()
.any(|(code, _)| code == issue_codes::LINT_AL_005),
"alias usage in QUALIFY should satisfy AL_005: {issues:?}"
);
}
#[test]
fn lint_al_005_alias_used_in_named_window_clause() {
let issues = run_lint(
"SELECT SUM(u.id) OVER w FROM users u JOIN orders o ON users.id = orders.user_id WINDOW w AS (PARTITION BY o.user_id)",
);
assert!(
!issues
.iter()
.any(|(code, _)| code == issue_codes::LINT_AL_005),
"alias usage in WINDOW clause should satisfy AL_005: {issues:?}"
);
}
#[test]
fn lint_al_005_alias_used_only_in_lateral_subquery_relation() {
let issues = run_lint(
"SELECT 1 \
FROM users u \
JOIN LATERAL (SELECT u.id) lx ON TRUE",
);
assert!(
!issues
.iter()
.any(|(code, _)| code == issue_codes::LINT_AL_005),
"alias usage inside LATERAL subquery relation should satisfy AL_005: {issues:?}"
);
}
#[test]
fn lint_al_005_alias_used_only_in_unnest_join_relation() {
let issues = run_lint(
"SELECT 1 \
FROM users u \
LEFT JOIN UNNEST(u.tags) tag ON TRUE",
);
assert!(
!issues
.iter()
.any(|(code, _)| code == issue_codes::LINT_AL_005),
"alias usage inside UNNEST join relation should satisfy AL_005: {issues:?}"
);
}
#[test]
fn lint_al_005_allows_unreferenced_subquery_alias() {
let issues = run_lint("SELECT * FROM (SELECT 1 AS a) subquery");
assert!(
!issues
.iter()
.any(|(code, _)| code == issue_codes::LINT_AL_005),
"derived-subquery aliases should not be treated as unused table aliases in AL_005: {issues:?}"
);
}
#[test]
fn lint_al_005_flags_inner_subquery_unused_alias() {
let issues = run_lint("SELECT * FROM (SELECT * FROM my_tbl AS foo)");
assert!(
issues
.iter()
.any(|(code, _)| code == issue_codes::LINT_AL_005),
"inner subquery table aliases should still be checked by AL_005: {issues:?}"
);
}
#[test]
fn lint_al_005_allows_postgres_generate_series_alias() {
let issues = run_lint_in_dialect(
"SELECT date_trunc('day', dd)::timestamp \
FROM generate_series('2022-02-01'::timestamp, NOW()::timestamp, '1 day'::interval) dd",
Dialect::Postgres,
);
assert!(
!issues
.iter()
.any(|(code, _)| code == issue_codes::LINT_AL_005),
"Postgres value-table-function aliases should be ignored for AL_005: {issues:?}"
);
}
#[test]
fn lint_al_005_flags_unused_snowflake_lateral_flatten_alias() {
let issues = run_lint_in_dialect(
"SELECT a.test1, a.test2, b.test3 \
FROM table1 AS a, \
LATERAL flatten(input => some_field) AS b, \
LATERAL flatten(input => b.value) AS c, \
LATERAL flatten(input => c.value) AS d, \
LATERAL flatten(input => d.value) AS e, \
LATERAL flatten(input => e.value) AS f",
Dialect::Snowflake,
);
assert!(
issues
.iter()
.any(|(code, _)| code == issue_codes::LINT_AL_005),
"unused aliases in chained Snowflake LATERAL FLATTEN factors should be flagged by AL_005: {issues:?}"
);
}
#[test]
fn lint_al_005_flags_unused_alias_inside_snowflake_delete_using_cte() {
let issues = run_lint_in_dialect(
"DELETE FROM MYTABLE1 \
USING ( \
WITH MYCTE AS (SELECT COLUMN2 FROM MYTABLE3 AS MT3) \
SELECT COLUMN3 FROM MYTABLE3 \
) X \
WHERE COLUMN1 = X.COLUMN3",
Dialect::Snowflake,
);
assert!(
issues
.iter()
.any(|(code, _)| code == issue_codes::LINT_AL_005),
"unused aliases inside Snowflake DELETE USING CTE scopes should be flagged by AL_005: {issues:?}"
);
}
#[test]
fn lint_al_005_allows_bigquery_to_json_string_table_alias_argument() {
let issues = run_lint_in_dialect(
"SELECT TO_JSON_STRING(t) FROM my_table AS t",
Dialect::Bigquery,
);
assert!(
!issues
.iter()
.any(|(code, _)| code == issue_codes::LINT_AL_005),
"BigQuery TO_JSON_STRING(table_alias) should count as alias usage for AL_005: {issues:?}"
);
}
#[test]
fn lint_al_005_flags_ansi_to_json_string_table_alias_argument() {
let issues = run_lint_in_dialect("SELECT TO_JSON_STRING(t) FROM my_table AS t", Dialect::Ansi);
assert!(
issues
.iter()
.any(|(code, _)| code == issue_codes::LINT_AL_005),
"ANSI TO_JSON_STRING(table_alias) should still be treated as unused alias in AL_005: {issues:?}"
);
}
#[test]
fn lint_al_005_redshift_qualify_after_from_counts_alias_usage() {
let issues = run_lint_in_dialect(
"SELECT * \
FROM store AS s \
INNER JOIN store_sales AS ss \
QUALIFY ROW_NUMBER() OVER (PARTITION BY ss.sold_date ORDER BY ss.sales_price DESC) <= 2",
Dialect::Redshift,
);
let al05_count = issues
.iter()
.filter(|(code, _)| code == issue_codes::LINT_AL_005)
.count();
assert_eq!(
al05_count, 1,
"Redshift QUALIFY after FROM should only preserve aliases referenced from QUALIFY: {issues:?}"
);
}
#[test]
fn lint_al_005_redshift_qualify_after_where_does_not_count_alias_usage() {
let issues = run_lint_in_dialect(
"SELECT * \
FROM store AS s \
INNER JOIN store_sales AS ss \
WHERE col = 1 \
QUALIFY ROW_NUMBER() OVER (PARTITION BY ss.sold_date ORDER BY ss.sales_price DESC) <= 2",
Dialect::Redshift,
);
let al05_count = issues
.iter()
.filter(|(code, _)| code == issue_codes::LINT_AL_005)
.count();
assert_eq!(
al05_count, 2,
"Redshift QUALIFY after WHERE should not preserve alias references for AL_005: {issues:?}"
);
}
#[test]
fn lint_al_005_redshift_qualify_unqualified_alias_prefixed_identifier_counts_usage() {
let issues = run_lint_in_dialect(
"SELECT * \
FROM #store_sales AS ss \
QUALIFY ROW_NUMBER() OVER (PARTITION BY ss_sold_date ORDER BY ss_sales_price DESC) <= 2",
Dialect::Redshift,
);
assert!(
!issues
.iter()
.any(|(code, _)| code == issue_codes::LINT_AL_005),
"Redshift QUALIFY unqualified alias-prefixed identifiers should count as usage in AL_005: {issues:?}"
);
}
#[test]
fn lint_al_005_allows_bigquery_implicit_array_table_reference() {
let issues = run_lint_in_dialect(
"WITH table_arr AS (SELECT [1,2,4,2] AS arr) \
SELECT arr \
FROM table_arr AS t, t.arr",
Dialect::Bigquery,
);
assert!(
!issues
.iter()
.any(|(code, _)| code == issue_codes::LINT_AL_005),
"BigQuery implicit array table references should count as alias usage in AL_005: {issues:?}"
);
}
#[test]
fn lint_al_005_allows_redshift_super_array_relation_reference() {
let issues = run_lint_in_dialect(
"SELECT my_column, my_array_value \
FROM my_schema.my_table AS t, t.super_array AS my_array_value",
Dialect::Redshift,
);
assert!(
!issues
.iter()
.any(|(code, _)| code == issue_codes::LINT_AL_005),
"Redshift super-array relation references should count as alias usage in AL_005: {issues:?}"
);
}
#[test]
fn lint_al_005_allows_repeat_referenced_table_aliases() {
let issues = run_lint(
"SELECT ROW_NUMBER() OVER(PARTITION BY a.object_id ORDER BY a.object_id) \
FROM sys.objects a \
CROSS JOIN sys.objects b \
CROSS JOIN sys.objects c",
);
assert!(
!issues
.iter()
.any(|(code, _)| code == issue_codes::LINT_AL_005),
"repeat-referenced table aliases should not trigger AL_005: {issues:?}"
);
}
#[test]
fn lint_rule_config_ambiguous_join_outer_mode() {
let issues = run_lint_with_config(
"SELECT * FROM foo LEFT JOIN bar ON foo.id = bar.id",
LintConfig {
enabled: true,
disabled_rules: vec![],
rule_configs: std::collections::BTreeMap::from([(
"ambiguous.join".to_string(),
serde_json::json!({"fully_qualify_join_types": "outer"}),
)]),
},
);
assert!(
issues.iter().any(|(code, _)| code == "LINT_AM_005"),
"outer join qualification mode should flag LEFT JOIN without OUTER: {issues:?}"
);
}
#[test]
fn lint_rule_config_ambiguous_join_outer_mode_right_join() {
let issues = run_lint_with_config(
"SELECT * FROM foo RIGHT JOIN bar ON foo.id = bar.id",
LintConfig {
enabled: true,
disabled_rules: vec![],
rule_configs: std::collections::BTreeMap::from([(
"ambiguous.join".to_string(),
serde_json::json!({"fully_qualify_join_types": "outer"}),
)]),
},
);
assert!(
issues.iter().any(|(code, _)| code == "LINT_AM_005"),
"outer join qualification mode should flag RIGHT JOIN without OUTER: {issues:?}"
);
}
#[test]
fn lint_rule_config_ambiguous_column_refs_explicit_mode() {
let issues = run_lint_with_config(
"SELECT foo, bar FROM fake_table GROUP BY 1, 2",
LintConfig {
enabled: true,
disabled_rules: vec![],
rule_configs: std::collections::BTreeMap::from([(
"LINT_AM_006".to_string(),
serde_json::json!({"group_by_and_order_by_style": "explicit"}),
)]),
},
);
assert!(
issues.iter().any(|(code, _)| code == "LINT_AM_006"),
"explicit mode should flag implicit GROUP BY positions: {issues:?}"
);
}
#[test]
fn lint_rule_config_aliasing_unused_case_sensitive() {
let issues = run_lint_with_config(
"SELECT zoo.id, b.id FROM users AS \"Zoo\" JOIN books b ON zoo.id = b.user_id",
LintConfig {
enabled: true,
disabled_rules: vec![],
rule_configs: std::collections::BTreeMap::from([(
"aliasing.unused".to_string(),
serde_json::json!({"alias_case_check": "case_sensitive"}),
)]),
},
);
assert!(
issues.iter().any(|(code, _)| code == "LINT_AL_005"),
"alias_case_check=case_sensitive should flag case-mismatched alias refs: {issues:?}"
);
}
#[test]
fn lint_rule_config_aliasing_unused_quoted_cs_naked_upper() {
let issues = run_lint_with_config(
"SELECT foo.id, b.id FROM users AS \"FOO\" JOIN books b ON foo.id = b.user_id",
LintConfig {
enabled: true,
disabled_rules: vec![],
rule_configs: std::collections::BTreeMap::from([(
"aliasing.unused".to_string(),
serde_json::json!({"alias_case_check": "quoted_cs_naked_upper"}),
)]),
},
);
assert!(
!issues.iter().any(|(code, _)| code == "LINT_AL_005"),
"alias_case_check=quoted_cs_naked_upper should upper-fold naked refs for quoted aliases: {issues:?}"
);
}
#[test]
fn lint_rule_config_aliasing_unused_quoted_cs_naked_lower() {
let issues = run_lint_with_config(
"SELECT FOO.id, b.id FROM users AS \"foo\" JOIN books b ON FOO.id = b.user_id",
LintConfig {
enabled: true,
disabled_rules: vec![],
rule_configs: std::collections::BTreeMap::from([(
"aliasing.unused".to_string(),
serde_json::json!({"alias_case_check": "quoted_cs_naked_lower"}),
)]),
},
);
assert!(
!issues.iter().any(|(code, _)| code == "LINT_AL_005"),
"alias_case_check=quoted_cs_naked_lower should lower-fold naked refs for quoted aliases: {issues:?}"
);
}
#[test]
fn lint_rule_config_aliasing_self_alias_case_sensitive() {
let issues = run_lint_with_config(
"SELECT a AS A FROM t",
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"}),
)]),
},
);
assert!(
!issues.iter().any(|(code, _)| code == "LINT_AL_009"),
"alias_case_check=case_sensitive should not flag case-mismatched self aliasing: {issues:?}"
);
}
#[test]
fn lint_rule_config_aliasing_self_alias_quoted_cs_naked_upper() {
let issues = run_lint_with_config(
"SELECT \"FOO\" AS foo FROM t",
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"}),
)]),
},
);
assert!(
issues.iter().any(|(code, _)| code == "LINT_AL_009"),
"quoted_cs_naked_upper should flag quoted-vs-unquoted self aliases matching after upper folding: {issues:?}"
);
}
#[test]
fn lint_rule_config_aliasing_self_alias_quoted_cs_naked_lower() {
let issues = run_lint_with_config(
"SELECT \"foo\" AS FOO FROM t",
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"}),
)]),
},
);
assert!(
issues.iter().any(|(code, _)| code == "LINT_AL_009"),
"quoted_cs_naked_lower should flag quoted-vs-unquoted self aliases matching after lower folding: {issues:?}"
);
}
#[test]
fn lint_rule_config_aliasing_unique_column_case_sensitive() {
let issues = run_lint_with_config(
"SELECT a, A FROM t",
LintConfig {
enabled: true,
disabled_rules: vec![],
rule_configs: std::collections::BTreeMap::from([(
"aliasing.unique.column".to_string(),
serde_json::json!({"alias_case_check": "case_sensitive"}),
)]),
},
);
assert!(
!issues.iter().any(|(code, _)| code == "LINT_AL_008"),
"alias_case_check=case_sensitive should allow case-mismatched projection aliases: {issues:?}"
);
}
#[test]
fn lint_rule_config_aliasing_unique_column_quoted_cs_naked_upper() {
let issues = run_lint_with_config(
"SELECT \"FOO\", foo FROM t",
LintConfig {
enabled: true,
disabled_rules: vec![],
rule_configs: std::collections::BTreeMap::from([(
"aliasing.unique.column".to_string(),
serde_json::json!({"alias_case_check": "quoted_cs_naked_upper"}),
)]),
},
);
assert!(
issues.iter().any(|(code, _)| code == "LINT_AL_008"),
"quoted_cs_naked_upper should flag quoted-vs-unquoted projection aliases matching after upper folding: {issues:?}"
);
}
#[test]
fn lint_rule_config_aliasing_unique_column_quoted_cs_naked_lower() {
let issues = run_lint_with_config(
"SELECT \"foo\", FOO FROM t",
LintConfig {
enabled: true,
disabled_rules: vec![],
rule_configs: std::collections::BTreeMap::from([(
"aliasing.unique.column".to_string(),
serde_json::json!({"alias_case_check": "quoted_cs_naked_lower"}),
)]),
},
);
assert!(
issues.iter().any(|(code, _)| code == "LINT_AL_008"),
"quoted_cs_naked_lower should flag quoted-vs-unquoted projection aliases matching after lower folding: {issues:?}"
);
}
#[test]
fn lint_rule_config_aliasing_unique_table_case_sensitive() {
let issues = run_lint_with_config(
"SELECT * FROM users a JOIN orders A ON a.id = A.user_id",
LintConfig {
enabled: true,
disabled_rules: vec![],
rule_configs: std::collections::BTreeMap::from([(
"aliasing.unique.table".to_string(),
serde_json::json!({"alias_case_check": "case_sensitive"}),
)]),
},
);
assert!(
!issues.iter().any(|(code, _)| code == "LINT_AL_004"),
"alias_case_check=case_sensitive should allow case-mismatched table aliases: {issues:?}"
);
}
#[test]
fn lint_rule_config_aliasing_unique_table_quoted_cs_naked_upper() {
let issues = run_lint_with_config(
"SELECT * FROM users AS \"FOO\" JOIN orders foo ON \"FOO\".id = foo.user_id",
LintConfig {
enabled: true,
disabled_rules: vec![],
rule_configs: std::collections::BTreeMap::from([(
"aliasing.unique.table".to_string(),
serde_json::json!({"alias_case_check": "quoted_cs_naked_upper"}),
)]),
},
);
assert!(
issues.iter().any(|(code, _)| code == "LINT_AL_004"),
"quoted_cs_naked_upper should flag quoted-vs-unquoted aliases that match after upper folding: {issues:?}"
);
}
#[test]
fn lint_rule_config_aliasing_unique_table_quoted_cs_naked_lower() {
let issues = run_lint_with_config(
"SELECT * FROM users AS \"foo\" JOIN orders FOO ON \"foo\".id = FOO.user_id",
LintConfig {
enabled: true,
disabled_rules: vec![],
rule_configs: std::collections::BTreeMap::from([(
"aliasing.unique.table".to_string(),
serde_json::json!({"alias_case_check": "quoted_cs_naked_lower"}),
)]),
},
);
assert!(
issues.iter().any(|(code, _)| code == "LINT_AL_004"),
"quoted_cs_naked_lower should flag quoted-vs-unquoted aliases that match after lower folding: {issues:?}"
);
}
#[test]
fn lint_rule_config_capitalisation_keywords_upper_policy() {
let issues = run_lint_with_config(
"select a from t",
LintConfig {
enabled: true,
disabled_rules: vec![],
rule_configs: std::collections::BTreeMap::from([(
"capitalisation.keywords".to_string(),
serde_json::json!({"capitalisation_policy": "upper"}),
)]),
},
);
assert!(
issues.iter().any(|(code, _)| code == "LINT_CP_001"),
"keyword upper policy should flag lowercase keywords: {issues:?}"
);
}
#[test]
fn lint_rule_config_capitalisation_keywords_ignore_words() {
let issues = run_lint_with_config(
"SELECT a from t",
LintConfig {
enabled: true,
disabled_rules: vec![],
rule_configs: std::collections::BTreeMap::from([(
"LINT_CP_001".to_string(),
serde_json::json!({"ignore_words": ["FROM"]}),
)]),
},
);
assert!(
!issues.iter().any(|(code, _)| code == "LINT_CP_001"),
"ignore_words should suppress configured keywords: {issues:?}"
);
}
#[test]
fn lint_rule_config_capitalisation_keywords_ignore_words_regex() {
let issues = run_lint_with_config(
"SELECT a from t",
LintConfig {
enabled: true,
disabled_rules: vec![],
rule_configs: std::collections::BTreeMap::from([(
"capitalisation.keywords".to_string(),
serde_json::json!({"ignore_words_regex": "^from$"}),
)]),
},
);
assert!(
!issues.iter().any(|(code, _)| code == "LINT_CP_001"),
"ignore_words_regex should suppress configured keyword patterns: {issues:?}"
);
}
#[test]
#[cfg(feature = "templating")]
fn lint_rule_config_capitalisation_keywords_ignore_templated_areas_true() {
let sql = "{{ \"select\" }} a\nFROM foo\nWHERE 1";
let issues = run_lint_with_config_in_dialect_with_jinja_template(
sql,
Dialect::Ansi,
LintConfig {
enabled: true,
disabled_rules: vec![],
rule_configs: std::collections::BTreeMap::from([
(
"capitalisation.keywords".to_string(),
serde_json::json!({"capitalisation_policy": "upper"}),
),
(
"core".to_string(),
serde_json::json!({"ignore_templated_areas": true}),
),
]),
},
);
assert!(
!issues.iter().any(|(code, _)| code == "LINT_CP_001"),
"ignore_templated_areas=true should ignore templated keyword tokens: {issues:?}"
);
}
#[test]
#[cfg(feature = "templating")]
fn lint_rule_config_capitalisation_keywords_ignore_templated_areas_false() {
let sql = "{{ \"select\" }} a\nFROM foo\nWHERE 1";
let issues = run_lint_with_config_in_dialect_with_jinja_template(
sql,
Dialect::Ansi,
LintConfig {
enabled: true,
disabled_rules: vec![],
rule_configs: std::collections::BTreeMap::from([
(
"capitalisation.keywords".to_string(),
serde_json::json!({"capitalisation_policy": "upper"}),
),
(
"core".to_string(),
serde_json::json!({"ignore_templated_areas": false}),
),
]),
},
);
assert!(
issues.iter().any(|(code, _)| code == "LINT_CP_001"),
"ignore_templated_areas=false should include templated keyword tokens: {issues:?}"
);
}
#[test]
fn lint_rule_config_capitalisation_identifiers_upper_policy() {
let issues = run_lint_with_config(
"SELECT col FROM t",
LintConfig {
enabled: true,
disabled_rules: vec![],
rule_configs: std::collections::BTreeMap::from([(
"capitalisation.identifiers".to_string(),
serde_json::json!({"extended_capitalisation_policy": "upper"}),
)]),
},
);
assert!(
issues.iter().any(|(code, _)| code == "LINT_CP_002"),
"identifier upper policy should flag lowercase identifiers: {issues:?}"
);
}
#[test]
fn lint_rule_config_capitalisation_identifiers_ignore_words_regex() {
let issues = run_lint_with_config(
"SELECT Col, col FROM t",
LintConfig {
enabled: true,
disabled_rules: vec![],
rule_configs: std::collections::BTreeMap::from([(
"capitalisation.identifiers".to_string(),
serde_json::json!({"ignore_words_regex": "^col$"}),
)]),
},
);
assert!(
!issues.iter().any(|(code, _)| code == "LINT_CP_002"),
"identifier ignore_words_regex should suppress matching identifiers: {issues:?}"
);
}
#[test]
fn lint_rule_config_capitalisation_identifiers_aliases_policy() {
let issues = run_lint_with_config(
"SELECT Col AS alias FROM t",
LintConfig {
enabled: true,
disabled_rules: vec![],
rule_configs: std::collections::BTreeMap::from([(
"capitalisation.identifiers".to_string(),
serde_json::json!({"unquoted_identifiers_policy": "aliases"}),
)]),
},
);
assert!(
!issues.iter().any(|(code, _)| code == "LINT_CP_002"),
"identifier aliases policy should ignore non-alias identifier case mix: {issues:?}"
);
}
#[test]
fn lint_rule_config_capitalisation_functions_lower_policy() {
let issues = run_lint_with_config(
"SELECT COUNT(x) FROM t",
LintConfig {
enabled: true,
disabled_rules: vec![],
rule_configs: std::collections::BTreeMap::from([(
"capitalisation.functions".to_string(),
serde_json::json!({"extended_capitalisation_policy": "lower"}),
)]),
},
);
assert!(
issues.iter().any(|(code, _)| code == "LINT_CP_003"),
"function lower policy should flag uppercase function names: {issues:?}"
);
}
#[test]
fn lint_rule_config_capitalisation_functions_ignore_words_regex() {
let issues = run_lint_with_config(
"SELECT COUNT(*), count(x) FROM t",
LintConfig {
enabled: true,
disabled_rules: vec![],
rule_configs: std::collections::BTreeMap::from([(
"capitalisation.functions".to_string(),
serde_json::json!({"ignore_words_regex": "^count$"}),
)]),
},
);
assert!(
!issues.iter().any(|(code, _)| code == "LINT_CP_003"),
"function ignore_words_regex should suppress matching function names: {issues:?}"
);
}
#[test]
fn lint_rule_config_capitalisation_literals_upper_policy() {
let issues = run_lint_with_config(
"SELECT true FROM t",
LintConfig {
enabled: true,
disabled_rules: vec![],
rule_configs: std::collections::BTreeMap::from([(
"LINT_CP_004".to_string(),
serde_json::json!({"extended_capitalisation_policy": "upper"}),
)]),
},
);
assert!(
issues.iter().any(|(code, _)| code == "LINT_CP_004"),
"literal upper policy should flag lowercase literals: {issues:?}"
);
}
#[test]
fn lint_rule_config_capitalisation_literals_ignore_words_regex() {
let issues = run_lint_with_config(
"SELECT NULL, true FROM t",
LintConfig {
enabled: true,
disabled_rules: vec![],
rule_configs: std::collections::BTreeMap::from([(
"capitalisation.literals".to_string(),
serde_json::json!({"ignore_words_regex": "^true$"}),
)]),
},
);
assert!(
!issues.iter().any(|(code, _)| code == "LINT_CP_004"),
"literal ignore_words_regex should suppress matching literals: {issues:?}"
);
}
#[test]
fn lint_rule_config_capitalisation_types_upper_policy() {
let issues = run_lint_with_config(
"CREATE TABLE t (a int)",
LintConfig {
enabled: true,
disabled_rules: vec![],
rule_configs: std::collections::BTreeMap::from([(
"capitalisation.types".to_string(),
serde_json::json!({"extended_capitalisation_policy": "upper"}),
)]),
},
);
assert!(
issues.iter().any(|(code, _)| code == "LINT_CP_005"),
"type upper policy should flag lowercase type names: {issues:?}"
);
}
#[test]
fn lint_rule_config_capitalisation_types_ignore_words_regex() {
let issues = run_lint_with_config(
"CREATE TABLE t (a INT, b varchar(10))",
LintConfig {
enabled: true,
disabled_rules: vec![],
rule_configs: std::collections::BTreeMap::from([(
"capitalisation.types".to_string(),
serde_json::json!({"ignore_words_regex": "^varchar$"}),
)]),
},
);
assert!(
!issues.iter().any(|(code, _)| code == "LINT_CP_005"),
"type ignore_words_regex should suppress matching type names: {issues:?}"
);
}
#[test]
fn lint_am_001_bare_union() {
let issues = run_lint("SELECT 1 UNION SELECT 2");
assert!(issues.iter().any(|(code, _)| code == "LINT_AM_002"));
}
#[test]
fn lint_am_001_union_all_ok() {
let issues = run_lint("SELECT 1 UNION ALL SELECT 2");
assert!(!issues.iter().any(|(code, _)| code == "LINT_AM_002"));
}
#[test]
fn lint_am_001_enabled_for_postgres_dialect() {
let issues = run_lint_in_dialect("SELECT 1 UNION SELECT 2", Dialect::Postgres);
assert!(
!issues.iter().any(|(code, _)| code == "LINT_AM_002"),
"AM_002 should be exempt for postgres dialect: {issues:?}"
);
}
#[test]
fn lint_am_001_enabled_for_redshift_dialect() {
let issues = run_lint_in_dialect("SELECT 1 UNION SELECT 2", Dialect::Redshift);
assert!(
issues.iter().any(|(code, _)| code == "LINT_AM_002"),
"AM_001 should be enabled for redshift dialect: {issues:?}"
);
}
#[test]
fn lint_am_002_limit_without_order_by() {
let issues = run_lint("SELECT * FROM t LIMIT 10");
assert!(issues.iter().any(|(code, _)| code == "LINT_AM_009"));
}
#[test]
fn lint_am_002_limit_with_order_by_ok() {
let issues = run_lint("SELECT * FROM t ORDER BY id LIMIT 10");
assert!(
!issues.iter().any(|(code, _)| code == "LINT_AM_009"),
"LIMIT with ORDER BY should not trigger AM_002: {issues:?}"
);
}
#[test]
fn lint_am_003_distinct_with_group_by() {
let issues = run_lint("SELECT DISTINCT col FROM t GROUP BY col");
assert!(issues.iter().any(|(code, _)| code == "LINT_AM_001"));
}
#[test]
fn lint_am_004_unknown_result_columns() {
let issues = run_lint("SELECT * FROM t");
assert!(issues.iter().any(|(code, _)| code == "LINT_AM_004"));
}
#[test]
fn lint_am_007_nested_join_alias_wildcard_set_mismatch() {
let issues = run_lint(
"SELECT j.* FROM ((SELECT a FROM t1) AS a1 JOIN (SELECT b FROM t2) AS b1 ON a1.a = b1.b) AS j UNION ALL SELECT x FROM t3",
);
assert!(issues.iter().any(|(code, _)| code == "LINT_AM_007"));
}
#[test]
fn lint_am_007_declared_cte_columns_resolve_set_width() {
let issues =
run_lint("WITH cte(a, b) AS (SELECT * FROM t) SELECT * FROM cte UNION SELECT c, d FROM t2");
assert!(
!issues.iter().any(|(code, _)| code == "LINT_AM_007"),
"declared CTE column list should resolve set-branch width for AM_007: {issues:?}"
);
}
#[test]
fn lint_am_007_declared_derived_alias_columns_set_mismatch() {
let issues = run_lint(
"SELECT t_alias.* FROM (SELECT * FROM t) AS t_alias(a, b, c) UNION SELECT d, e FROM t2",
);
assert!(issues.iter().any(|(code, _)| code == "LINT_AM_007"));
}
#[test]
fn lint_am_007_nested_join_using_width_resolves_for_set_comparison() {
let issues = run_lint(
"SELECT j.* FROM ((SELECT a FROM t1) AS a1 JOIN (SELECT a FROM t2) AS b1 USING(a)) AS j UNION ALL SELECT x FROM t3",
);
assert!(
!issues.iter().any(|(code, _)| code == "LINT_AM_007"),
"resolved USING-join width should avoid AM_007 mismatch: {issues:?}"
);
}
#[test]
fn lint_am_007_nested_join_natural_width_resolves_for_set_comparison() {
let issues = run_lint(
"SELECT j.* FROM ((SELECT a FROM t1) AS a1 NATURAL JOIN (SELECT a FROM t2) AS b1) AS j UNION ALL SELECT x FROM t3",
);
assert!(
!issues.iter().any(|(code, _)| code == "LINT_AM_007"),
"resolved NATURAL-join width should avoid AM_007 mismatch: {issues:?}"
);
}
#[test]
fn lint_am_007_nested_join_natural_width_unknown_does_not_trigger_for_set_comparison() {
let issues = run_lint(
"SELECT j.* FROM ((SELECT * FROM t1) AS a1 NATURAL JOIN (SELECT a FROM t2) AS b1) AS j UNION ALL SELECT x FROM t3",
);
assert!(
!issues.iter().any(|(code, _)| code == "LINT_AM_007"),
"unknown NATURAL-join width should defer AM_007 mismatch checks: {issues:?}"
);
}
#[test]
fn lint_cv_002_count_one() {
let issues = run_lint("SELECT COUNT(1) FROM t");
assert!(issues.iter().any(|(code, _)| code == "LINT_CV_004"));
}
#[test]
fn lint_cv_003_null_comparison() {
let issues = run_lint("SELECT * FROM t WHERE a = NULL");
assert!(issues.iter().any(|(code, _)| code == "LINT_CV_005"));
}
#[test]
fn lint_cv_004_right_join() {
let issues = run_lint("SELECT * FROM a RIGHT JOIN b ON a.id = b.id");
assert!(issues.iter().any(|(code, _)| code == "LINT_CV_008"));
}
#[test]
fn lint_cv_012_inner_join_without_on_with_where_predicate() {
let issues = run_lint("SELECT a.x, b.y FROM a INNER JOIN b WHERE a.id = b.id");
assert!(issues.iter().any(|(code, _)| code == "LINT_CV_012"));
}
#[test]
fn lint_cv_012_multi_join_not_all_references_is_not_flagged() {
let issues = run_lint("SELECT a.id FROM a JOIN b JOIN c WHERE a.a = b.a AND b.b > 1");
assert!(
!issues.iter().any(|(code, _)| code == "LINT_CV_012"),
"CV_012 should not fire when not all naked joins are represented by WHERE join predicates: {issues:?}"
);
}
#[test]
fn lint_st_001_unused_cte() {
let issues = run_lint("WITH unused AS (SELECT 1) SELECT 2");
assert!(issues.iter().any(|(code, _)| code == "LINT_ST_003"));
}
#[test]
fn lint_st_002_else_null() {
let issues = run_lint("SELECT CASE WHEN x > 1 THEN 'a' ELSE NULL END FROM t");
assert!(issues.iter().any(|(code, _)| code == "LINT_ST_001"));
}
#[test]
fn lint_st_003_flattenable_else_case() {
let sql = "SELECT CASE WHEN species = 'Rat' THEN 'Squeak' ELSE CASE WHEN species = 'Dog' THEN 'Woof' END END AS sound FROM mytable";
let issues = run_lint(sql);
assert!(issues.iter().any(|(code, _)| code == "LINT_ST_004"));
}
#[test]
fn lint_st_004_using_join() {
let issues = run_lint("SELECT * FROM a JOIN b USING (id)");
assert!(issues.iter().any(|(code, _)| code == "LINT_ST_007"));
}
#[test]
fn lint_al_001_implicit_alias() {
let issues = run_lint("SELECT a + b FROM t");
assert!(issues.iter().any(|(code, _)| code == "LINT_AL_003"));
}
#[test]
fn lint_al_001_explicit_alias_ok() {
let issues = run_lint("SELECT a + b AS total FROM t");
assert!(!issues.iter().any(|(code, _)| code == "LINT_AL_003"));
}
#[test]
fn lint_cv_001_ifnull_usage() {
let issues = run_lint("SELECT IFNULL(x, 'default') FROM t");
assert!(issues.iter().any(|(code, _)| code == "LINT_CV_002"));
}
#[test]
fn lint_clean_query_no_issues() {
let issues = run_lint_with_config(
"SELECT id, name FROM users WHERE active = true\n",
LintConfig {
enabled: true,
disabled_rules: vec!["LINT_LT_009".to_string(), "LINT_LT_014".to_string()],
rule_configs: std::collections::BTreeMap::new(),
},
);
assert!(
issues.is_empty(),
"clean query should produce no lint issues: {issues:?}"
);
}
#[test]
fn lint_multiple_rules_on_single_query() {
let issues = run_lint("SELECT a + b UNION SELECT c + d");
let codes: Vec<&str> = issues.iter().map(|(c, _)| c.as_str()).collect();
assert!(
codes.contains(&"LINT_AM_002"),
"expected bare union: {codes:?}"
);
assert!(
codes.contains(&"LINT_AL_003"),
"expected implicit alias: {codes:?}"
);
}
#[test]
fn lint_unused_cte_case_insensitive() {
let issues = run_lint("WITH My_Cte AS (SELECT 1) SELECT * FROM my_cte");
assert!(
!issues.iter().any(|(code, _)| code == "LINT_ST_003"),
"case-insensitive CTE name should be recognized as used"
);
}
#[test]
fn lint_chained_ctes_all_used() {
let issues = run_lint(
"WITH a AS (SELECT 1 AS x), b AS (SELECT * FROM a), c AS (SELECT * FROM b) SELECT * FROM c",
);
assert!(
!issues.iter().any(|(code, _)| code == "LINT_ST_003"),
"chained CTEs should all count as used"
);
}
#[test]
fn lint_bare_union_in_create_view() {
let issues = run_lint("CREATE VIEW v AS SELECT 1 UNION SELECT 2");
assert!(issues.iter().any(|(code, _)| code == "LINT_AM_002"));
}
#[test]
fn lint_else_null_nested_both_detected() {
let issues =
run_lint("SELECT CASE WHEN a THEN CASE WHEN b THEN 1 ELSE NULL END ELSE NULL END FROM t");
let st002_count = issues
.iter()
.filter(|(code, _)| code == "LINT_ST_001")
.count();
assert_eq!(st002_count, 2, "both nested ELSE NULLs should be detected");
}
#[test]
fn lint_disable_multiple_rules() {
let issues = run_lint_with_config(
"SELECT a + b UNION SELECT c + d",
LintConfig {
enabled: true,
disabled_rules: vec!["LINT_AM_002".to_string(), "LINT_AL_003".to_string()],
rule_configs: std::collections::BTreeMap::new(),
},
);
assert!(
!issues
.iter()
.any(|(code, _)| code == "LINT_AM_002" || code == "LINT_AL_003"),
"disabled rules should not appear: {issues:?}"
);
}
#[test]
fn lint_count_one_in_having() {
let issues = run_lint("SELECT col FROM t GROUP BY col HAVING COUNT(1) > 5");
assert!(issues.iter().any(|(code, _)| code == "LINT_CV_004"));
}
#[test]
fn lint_limit_in_subquery_without_order_by() {
let issues = run_lint("SELECT * FROM (SELECT * FROM t LIMIT 10) AS sub");
assert!(issues.iter().any(|(code, _)| code == "LINT_AM_009"));
}
#[test]
fn lint_clean_complex_query_no_issues() {
let issues = run_lint_with_config(
"WITH recent_orders AS (SELECT * FROM orders ORDER BY user_id LIMIT 100) \
SELECT u.name, COUNT(*) AS order_count \
FROM users u \
JOIN recent_orders o ON u.id = o.user_id \
GROUP BY u.name \
ORDER BY order_count DESC\n",
LintConfig {
enabled: true,
disabled_rules: vec![
"LINT_LT_009".to_string(),
"LINT_LT_008".to_string(),
"LINT_LT_005".to_string(),
"LINT_LT_002".to_string(),
"LINT_AL_001".to_string(),
"LINT_AL_007".to_string(),
"LINT_AM_005".to_string(),
"LINT_ST_011".to_string(),
],
rule_configs: std::collections::BTreeMap::new(),
},
);
assert!(
issues.is_empty(),
"well-structured query should have no lint issues: {issues:?}"
);
}
#[test]
fn lint_config_serialization() {
let config = LintConfig {
enabled: true,
disabled_rules: vec!["LINT_AM_002".to_string()],
rule_configs: std::collections::BTreeMap::new(),
};
let json = serde_json::to_string(&config).unwrap();
assert!(json.contains("\"disabledRules\""));
let deserialized: LintConfig = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized.disabled_rules, vec!["LINT_AM_002"]);
}
#[test]
fn lint_config_in_analyze_request() {
let json = r#"{
"sql": "SELECT 1",
"dialect": "generic",
"options": {
"lint": {
"enabled": true,
"disabledRules": ["LINT_AM_002"]
}
}
}"#;
let request: AnalyzeRequest = serde_json::from_str(json).unwrap();
let lint = request.options.unwrap().lint.unwrap();
assert!(lint.enabled);
assert_eq!(lint.disabled_rules, vec!["LINT_AM_002"]);
}
#[test]
fn lint_am_004_known_columns_ok() {
let issues = run_lint("WITH cte AS (SELECT a, b FROM t) SELECT * FROM cte");
assert!(
!issues.iter().any(|(code, _)| code == "LINT_AM_004"),
"known output width should not trigger AM_004: {issues:?}"
);
}
#[test]
fn lint_am_004_nested_join_alias_known_columns_ok() {
let issues = run_lint(
"SELECT j.* FROM ((SELECT a FROM t1) AS a1 JOIN (SELECT b FROM t2) AS b1 ON a1.a = b1.b) AS j",
);
assert!(
!issues.iter().any(|(code, _)| code == "LINT_AM_004"),
"resolved nested-join alias wildcard should not trigger AM_004: {issues:?}"
);
}
#[test]
fn lint_am_004_declared_cte_columns_known_width_ok() {
let issues = run_lint("WITH cte(a, b) AS (SELECT * FROM t) SELECT * FROM cte");
assert!(
!issues.iter().any(|(code, _)| code == "LINT_AM_004"),
"declared CTE column list should resolve wildcard width for AM_004: {issues:?}"
);
}
#[test]
fn lint_am_004_declared_derived_alias_columns_known_width_ok() {
let issues = run_lint("SELECT t_alias.* FROM (SELECT * FROM t) AS t_alias(a, b)");
assert!(
!issues.iter().any(|(code, _)| code == "LINT_AM_004"),
"declared derived alias columns should resolve wildcard width for AM_004: {issues:?}"
);
}
#[test]
fn lint_am_004_nested_join_using_width_known_columns_ok() {
let issues = run_lint(
"SELECT j.* FROM ((SELECT a FROM t1) AS a1 JOIN (SELECT a FROM t2) AS b1 USING(a)) AS j",
);
assert!(
!issues.iter().any(|(code, _)| code == "LINT_AM_004"),
"resolved USING-join width should avoid AM_004 unknown-width warning: {issues:?}"
);
}
#[test]
fn lint_am_004_nested_join_natural_width_resolved_ok() {
let issues = run_lint(
"SELECT j.* FROM ((SELECT a FROM t1) AS a1 NATURAL JOIN (SELECT a FROM t2) AS b1) AS j",
);
assert!(
!issues.iter().any(|(code, _)| code == "LINT_AM_004"),
"known NATURAL-join output width should avoid AM_004 warning: {issues:?}"
);
}
#[test]
fn lint_am_004_nested_join_natural_width_unknown_flags() {
let issues = run_lint(
"SELECT j.* FROM ((SELECT * FROM t1) AS a1 NATURAL JOIN (SELECT a FROM t2) AS b1) AS j",
);
assert!(issues.iter().any(|(code, _)| code == "LINT_AM_004"));
}
#[test]
fn lint_cv_003_is_null_ok() {
let issues = run_lint("SELECT * FROM t WHERE a IS NULL");
assert!(
!issues.iter().any(|(code, _)| code == "LINT_CV_005"),
"IS NULL should not trigger CV_003: {issues:?}"
);
}
#[test]
fn lint_cv_004_left_join_ok() {
let issues = run_lint("SELECT * FROM a LEFT JOIN b ON a.id = b.id");
assert!(
!issues.iter().any(|(code, _)| code == "LINT_CV_008"),
"LEFT JOIN should not trigger CV_004: {issues:?}"
);
}
#[test]
fn lint_st_004_on_join_ok() {
let issues = run_lint("SELECT * FROM a JOIN b ON a.id = b.id");
assert!(
!issues.iter().any(|(code, _)| code == "LINT_ST_007"),
"JOIN ON should not trigger ST_004: {issues:?}"
);
}
#[test]
fn lint_al_003_explicit_aliases_ok() {
let issues =
run_lint("SELECT a.id, b.name FROM users AS a JOIN orders AS b ON a.id = b.user_id");
assert!(
!issues.iter().any(|(code, _)| code == "LINT_AL_003"),
"explicit aliases should not trigger AL_003: {issues:?}"
);
}
#[test]
fn lint_al_004_duplicate_implicit_table_name_aliases() {
let issues = run_lint(
"SELECT * FROM analytics.foo JOIN reporting.foo ON analytics.foo.id = reporting.foo.id",
);
assert!(
issues.iter().any(|(code, _)| code == "LINT_AL_004"),
"duplicate implicit table-name aliases should trigger AL_004: {issues:?}"
);
}
#[test]
fn lint_al_004_duplicate_alias_between_parent_and_subquery_scope() {
let issues =
run_lint("SELECT * FROM (SELECT * FROM users a) s JOIN orders a ON s.id = a.user_id");
assert!(
issues.iter().any(|(code, _)| code == "LINT_AL_004"),
"parent/subquery alias collisions should trigger AL_004: {issues:?}"
);
}
#[test]
fn lint_al_004_duplicate_alias_between_outer_scope_and_where_subquery() {
let issues = run_lint("SELECT * FROM tbl AS t WHERE t.val IN (SELECT t.val FROM tbl2 AS t)");
assert!(
issues.iter().any(|(code, _)| code == "LINT_AL_004"),
"outer/WHERE-subquery alias collisions should trigger AL_004: {issues:?}"
);
}
#[test]
fn lint_al_004_duplicate_implicit_table_name_in_where_subquery() {
let issues = run_lint("SELECT * FROM tbl WHERE val IN (SELECT tbl.val FROM tbl)");
assert!(
issues.iter().any(|(code, _)| code == "LINT_AL_004"),
"implicit table-name collisions across WHERE subqueries should trigger AL_004: {issues:?}"
);
}
#[test]
fn lint_al_001_bigquery_merge_requires_explicit_aliases() {
let issues = run_lint_in_dialect(
"MERGE dataset.inventory t USING dataset.newarrivals s ON t.product = s.product WHEN MATCHED THEN UPDATE SET quantity = t.quantity + s.quantity",
Dialect::Bigquery,
);
assert!(
issues.iter().any(|(code, _)| code == "LINT_AL_001"),
"BigQuery MERGE aliases without AS should trigger AL_001: {issues:?}"
);
}
#[test]
fn lint_al_002_tsql_assignment_alias_syntax_is_allowed() {
let issues = run_lint_in_dialect("SELECT alias1 = col1", Dialect::Mssql);
assert!(
!issues.iter().any(|(code, _)| code == "LINT_AL_002"),
"TSQL assignment-style aliases should not trigger AL_002: {issues:?}"
);
}
#[test]
fn lint_al_008_duplicate_unaliased_column_reference() {
let issues = run_lint("SELECT foo, foo FROM t");
assert!(
issues.iter().any(|(code, _)| code == "LINT_AL_008"),
"duplicate unaliased projection refs should trigger AL_008: {issues:?}"
);
}
#[test]
fn lint_am_005_order_by_name_ok() {
let issues = run_lint("SELECT name FROM t ORDER BY name");
assert!(
!issues.iter().any(|(code, _)| code == "LINT_AM_003"),
"ORDER BY column name should not trigger AM_005: {issues:?}"
);
}
#[test]
fn lint_am_005_mixed_order_by_direction() {
let issues = run_lint("SELECT a, b FROM t ORDER BY a, b DESC");
assert!(
issues.iter().any(|(code, _)| code == "LINT_AM_003"),
"mixed ORDER BY direction should trigger AM_005: {issues:?}"
);
}
#[test]
fn lint_cp_001_consistent_case_ok() {
let issues = run_lint("SELECT id FROM users WHERE active = true");
assert!(
!issues.iter().any(|(code, _)| code == "LINT_CP_001"),
"consistent keyword case should not trigger CP_001: {issues:?}"
);
}
#[test]
fn lint_rf_003_mixed_qualified_wildcard_and_unqualified_ref() {
let issues = run_lint("SELECT t.*, id FROM t");
assert!(
issues.iter().any(|(code, _)| code == "LINT_RF_003"),
"qualified wildcard mixed with unqualified refs should trigger RF_003: {issues:?}"
);
}
#[test]
fn lint_st_010_no_constant_expression_ok() {
let issues = run_lint("SELECT * FROM t WHERE status = 'active'");
assert!(
!issues.iter().any(|(code, _)| code == "LINT_ST_010"),
"normal WHERE should not trigger ST_010: {issues:?}"
);
}
#[test]
fn lint_st_010_non_equality_literal_comparison_ok() {
let issues = run_lint("SELECT * FROM t WHERE 1 < 2");
assert!(
!issues.iter().any(|(code, _)| code == "LINT_ST_010"),
"non-equality literal comparison should not trigger ST_010: {issues:?}"
);
}
#[test]
fn lint_st_010_flags_self_comparison_with_inequality_operators() {
let issues = run_lint("SELECT * FROM t WHERE a < a OR b >= b");
let st_010_count = issues
.iter()
.filter(|(code, _)| code == "LINT_ST_010")
.count();
assert_eq!(
st_010_count, 2,
"self-comparisons using inequality operators should trigger ST_010 per occurrence: {issues:?}"
);
}
#[test]
fn lint_st_010_reports_each_constant_expression_occurrence() {
let issues = run_lint("SELECT * FROM t WHERE a = a AND b = b");
let st_010_count = issues
.iter()
.filter(|(code, _)| code == "LINT_ST_010")
.count();
assert_eq!(
st_010_count, 2,
"expected two ST_010 violations: {issues:?}"
);
}
#[test]
fn lint_st_010_flags_equal_string_concat_expression() {
let issues = run_lint("SELECT * FROM t WHERE 'A' || 'B' = 'A' || 'B'");
assert!(
issues.iter().any(|(code, _)| code == "LINT_ST_010"),
"equivalent string-concat expressions should trigger ST_010: {issues:?}"
);
}
#[test]
fn lint_st_011_inner_join_not_checked() {
let issues = run_lint("SELECT a.id FROM a INNER JOIN b ON a.id = b.id");
assert!(
!issues.iter().any(|(code, _)| code == "LINT_ST_011"),
"inner joins should not trigger ST_011 by default: {issues:?}"
);
}
#[test]
fn lint_st_011_unqualified_wildcard_counts_as_reference() {
let issues = run_lint("SELECT * FROM a LEFT JOIN b ON a.id = b.id");
assert!(
!issues.iter().any(|(code, _)| code == "LINT_ST_011"),
"unqualified wildcard should count as joined-source usage: {issues:?}"
);
}
#[test]
fn lint_st_011_snowflake_qualified_wildcard_exclude_counts_as_reference() {
let issues = run_lint_in_dialect(
"select \
simulation_source_data_reference.*, \
sourcings.* exclude sourcing_job_id \
from simulation_source_data_reference \
left join sourcings \
on simulation_source_data_reference.sourcing_job_id = sourcings.sourcing_job_id",
Dialect::Snowflake,
);
assert!(
!issues.iter().any(|(code, _)| code == "LINT_ST_011"),
"Snowflake qualified wildcard EXCLUDE should count as joined-source usage: {issues:?}"
);
}
#[test]
fn lint_st_011_query_order_by_reference_counts_as_usage() {
let issues = run_lint("SELECT a.id FROM a LEFT JOIN b ON a.id = b.id ORDER BY b.id");
assert!(
!issues.iter().any(|(code, _)| code == "LINT_ST_011"),
"query-level ORDER BY references should count as joined-source usage: {issues:?}"
);
}
#[test]
fn lint_st_011_named_window_reference_counts_as_usage() {
let issues =
run_lint("SELECT SUM(a.value) OVER w FROM a LEFT JOIN b ON a.id = b.id WINDOW w AS (PARTITION BY b.group_key)");
assert!(
!issues.iter().any(|(code, _)| code == "LINT_ST_011"),
"named WINDOW clause references should count as joined-source usage: {issues:?}"
);
}
#[test]
fn lint_st_011_named_window_unqualified_reference_defers_check() {
let issues =
run_lint("SELECT SUM(a.value) OVER w FROM a LEFT JOIN b ON a.id = b.id WINDOW w AS (PARTITION BY group_key)");
assert!(
!issues.iter().any(|(code, _)| code == "LINT_ST_011"),
"unqualified named WINDOW references should defer ST_011: {issues:?}"
);
}
#[test]
fn lint_st_011_distinct_on_reference_counts_as_usage() {
let issues = run_lint("SELECT DISTINCT ON (b.id) a.id FROM a LEFT JOIN b ON a.id = b.id");
assert!(
!issues.iter().any(|(code, _)| code == "LINT_ST_011"),
"DISTINCT ON joined-source references should count as usage: {issues:?}"
);
}
#[test]
fn lint_st_011_distinct_on_unqualified_reference_defers_check() {
let issues = run_lint("SELECT DISTINCT ON (id) a.id FROM a LEFT JOIN b ON a.id = b.id");
assert!(
!issues.iter().any(|(code, _)| code == "LINT_ST_011"),
"unqualified DISTINCT ON references should defer ST_011: {issues:?}"
);
}
#[test]
fn lint_st_011_cluster_by_reference_counts_as_usage() {
let issues = run_lint("SELECT a.id FROM a LEFT JOIN b ON a.id = b.id CLUSTER BY b.id");
assert!(
!issues.iter().any(|(code, _)| code == "LINT_ST_011"),
"CLUSTER BY joined-source references should count as usage: {issues:?}"
);
}
#[test]
fn lint_st_011_distribute_by_reference_counts_as_usage() {
let issues = run_lint("SELECT a.id FROM a LEFT JOIN b ON a.id = b.id DISTRIBUTE BY b.id");
assert!(
!issues.iter().any(|(code, _)| code == "LINT_ST_011"),
"DISTRIBUTE BY joined-source references should count as usage: {issues:?}"
);
}
#[test]
fn lint_st_011_mysql_backtick_quoted_joined_source_reference_counts_as_usage() {
let issues = run_lint_in_dialect(
"SELECT `test`.one, `test-2`.two FROM `test` LEFT JOIN `test-2` ON `test`.id = `test-2`.id",
Dialect::Mysql,
);
assert!(
!issues.iter().any(|(code, _)| code == "LINT_ST_011"),
"backtick-quoted joined-source references should count as usage: {issues:?}"
);
}
#[test]
fn lint_st_011_mssql_bracket_quoted_joined_source_reference_counts_as_usage() {
let issues = run_lint_in_dialect(
"SELECT [test].one, [test-2].two FROM [test] LEFT JOIN [test-2] ON [test].id = [test-2].id",
Dialect::Mssql,
);
assert!(
!issues.iter().any(|(code, _)| code == "LINT_ST_011"),
"bracket-quoted joined-source references should count as usage: {issues:?}"
);
}
#[test]
fn lint_st_011_hive_lateral_view_reference_counts_as_usage() {
let issues = run_lint_in_dialect(
"SELECT a.id FROM a LEFT JOIN b ON a.id = b.id LATERAL VIEW explode(b.items) lv AS item",
Dialect::Hive,
);
assert!(
!issues.iter().any(|(code, _)| code == "LINT_ST_011"),
"LATERAL VIEW joined-source references should count as usage: {issues:?}"
);
}
#[test]
fn lint_st_011_hive_lateral_view_unqualified_reference_defers_check() {
let issues = run_lint_in_dialect(
"SELECT a.id FROM a LEFT JOIN b ON a.id = b.id LATERAL VIEW explode(items) lv AS item",
Dialect::Hive,
);
assert!(
!issues.iter().any(|(code, _)| code == "LINT_ST_011"),
"unqualified LATERAL VIEW references should defer ST_011: {issues:?}"
);
}
#[test]
fn lint_st_011_connect_by_reference_counts_as_usage() {
let issues = run_lint_in_dialect(
"SELECT a.id FROM a LEFT JOIN b ON a.id = b.id START WITH b.id IS NOT NULL CONNECT BY PRIOR a.id = b.id",
Dialect::Snowflake,
);
assert!(
!issues.iter().any(|(code, _)| code == "LINT_ST_011"),
"CONNECT BY joined-source references should count as usage: {issues:?}"
);
}
#[test]
fn lint_st_011_connect_by_unqualified_reference_defers_check() {
let issues = run_lint_in_dialect(
"SELECT a.id FROM a LEFT JOIN b ON a.id = b.id START WITH id IS NOT NULL CONNECT BY PRIOR a.id = b.id",
Dialect::Snowflake,
);
assert!(
!issues.iter().any(|(code, _)| code == "LINT_ST_011"),
"unqualified CONNECT BY references should defer ST_011: {issues:?}"
);
}
#[test]
fn lint_st_011_does_not_flag_base_from_with_using_join() {
let issues = run_lint("SELECT b.id FROM a LEFT JOIN b USING(id)");
assert!(
!issues.iter().any(|(code, _)| code == "LINT_ST_011"),
"base FROM source should not be considered an unused joined source: {issues:?}"
);
}
#[test]
fn lint_st_011_flags_multi_root_unused_outer_join_source() {
let issues = run_lint("SELECT a.id FROM a, b LEFT JOIN c ON b.id = c.id");
assert!(
issues.iter().any(|(code, _)| code == "LINT_ST_011"),
"unused joined source in multi-root FROM should trigger ST_011: {issues:?}"
);
}
#[test]
fn lint_st_011_allows_unnest_chain_reference_between_join_relations() {
let issues = run_lint(
"SELECT ft.id, n.generic_field FROM fact_table AS ft LEFT JOIN UNNEST(ft.generic_array) AS g LEFT JOIN UNNEST(g.nested_array) AS n",
);
assert!(
!issues.iter().any(|(code, _)| code == "LINT_ST_011"),
"UNNEST join-chain relation references should count as joined-source usage: {issues:?}"
);
}
#[test]
fn lint_lt_007_cte_bracket_missing() {
let issues = run_lint("WITH cte AS (\n SELECT 1) SELECT * FROM cte");
assert!(
issues
.iter()
.any(|(code, _)| code == issue_codes::LINT_LT_007),
"expected {}: {issues:?}",
issue_codes::LINT_LT_007,
);
}
#[test]
fn lint_lt_002_first_line_indented() {
let issues = run_lint(" SELECT 1");
assert!(
issues
.iter()
.any(|(code, _)| code == issue_codes::LINT_LT_002),
"expected {} for first-line indentation: {issues:?}",
issue_codes::LINT_LT_002,
);
}
#[test]
#[cfg(feature = "templating")]
fn lint_lt_007_jinja_whitespace_consumption_expression_on_own_line_passes() {
let sql = "with cte as (\n select 1\n {{- ' from i_consume_whitespace ' -}}\n) select * from cte";
let issues = run_lint_in_dialect_with_jinja_template(sql, Dialect::Ansi);
assert!(
!issues
.iter()
.any(|(code, _)| code == issue_codes::LINT_LT_007),
"expected no {} for whitespace-consuming template line with own-line close: {issues:?}",
issue_codes::LINT_LT_007,
);
}
#[test]
#[cfg(feature = "templating")]
fn lint_lt_007_jinja_whitespace_consumption_comment_on_own_line_passes() {
let sql = "with cte as (\n select 1\n {#- consumed -#}\n) select * from cte";
let issues = run_lint_in_dialect_with_jinja_template(sql, Dialect::Ansi);
assert!(
!issues
.iter()
.any(|(code, _)| code == issue_codes::LINT_LT_007),
"expected no {} for whitespace-consuming template comment with own-line close: {issues:?}",
issue_codes::LINT_LT_007,
);
}
#[test]
#[cfg(feature = "templating")]
fn lint_lt_007_jinja_whitespace_consumption_same_line_close_still_flags() {
let sql = "with cte as (\n select 1\n {%- if False -%}{%- endif -%}) select * from cte";
let issues = run_lint_in_dialect_with_jinja_template(sql, Dialect::Ansi);
assert!(
issues
.iter()
.any(|(code, _)| code == issue_codes::LINT_LT_007),
"expected {} for same-line close after whitespace-consuming template block: {issues:?}",
issue_codes::LINT_LT_007,
);
}
#[test]
fn lint_lt_012_multiple_trailing_newlines() {
let issues = run_lint("SELECT 1\nFROM t\n\n");
assert!(
issues
.iter()
.any(|(code, _)| code == issue_codes::LINT_LT_012),
"expected {} for multiple trailing newlines: {issues:?}",
issue_codes::LINT_LT_012,
);
}
#[test]
fn lint_jj_001_missing_padding_before_statement_close_tag() {
let issues = run_lint("SELECT '{% for x in y%}' AS templated");
assert!(
issues
.iter()
.any(|(code, _)| code == issue_codes::LINT_JJ_001),
"expected {} for missing close-tag padding: {issues:?}",
issue_codes::LINT_JJ_001,
);
}
#[test]
fn lint_rf_004_keyword_identifier() {
let issues = run_lint("SELECT sum.id FROM users AS sum");
assert!(
issues
.iter()
.any(|(code, _)| code == issue_codes::LINT_RF_004),
"expected {}: {issues:?}",
issue_codes::LINT_RF_004,
);
}
#[test]
fn lint_rf_004_keyword_cte_identifier() {
let issues = run_lint("WITH sum AS (SELECT 1 AS value) SELECT value FROM sum");
assert!(
issues
.iter()
.any(|(code, _)| code == issue_codes::LINT_RF_004),
"expected {} for keyword CTE identifiers: {issues:?}",
issue_codes::LINT_RF_004,
);
}
#[test]
fn lint_rf_005_special_chars() {
let issues = run_lint("SELECT \"bad-name\" FROM t");
assert!(
issues
.iter()
.any(|(code, _)| code == issue_codes::LINT_RF_005),
"expected {} for quoted identifier special chars: {issues:?}",
issue_codes::LINT_RF_005,
);
}
#[test]
fn lint_rf_002_flags_projection_self_alias_in_multi_source_query() {
let issues = run_lint("SELECT foo AS foo FROM a LEFT JOIN b ON a.id = b.id");
assert!(
issues
.iter()
.any(|(code, _)| code == issue_codes::LINT_RF_002),
"expected {} for self-alias projection in multi-source query: {issues:?}",
issue_codes::LINT_RF_002,
);
}
#[test]
fn lint_rf_002_allows_later_projection_reference_to_previous_alias() {
let issues = run_lint("SELECT a.bar AS baz, baz FROM a LEFT JOIN b ON a.id = b.id");
assert!(
!issues
.iter()
.any(|(code, _)| code == issue_codes::LINT_RF_002),
"did not expect {} for valid later alias reference: {issues:?}",
issue_codes::LINT_RF_002,
);
}
#[test]
fn lint_rf_002_allows_bigquery_date_part_function_argument() {
let issues = run_lint_in_dialect(
"SELECT timestamp_trunc(a.ts, month) AS t FROM a JOIN b ON a.id = b.id",
Dialect::Bigquery,
);
assert!(
!issues
.iter()
.any(|(code, _)| code == issue_codes::LINT_RF_002),
"datepart function-argument keywords should not trigger {}: {issues:?}",
issue_codes::LINT_RF_002,
);
}
#[test]
fn lint_rf_002_allows_snowflake_datediff_date_part_argument() {
let issues = run_lint_in_dialect(
"SELECT datediff(year, a.column1, b.column2) FROM a JOIN b ON a.id = b.id",
Dialect::Snowflake,
);
assert!(
!issues
.iter()
.any(|(code, _)| code == issue_codes::LINT_RF_002),
"datediff datepart keywords should not trigger {}: {issues:?}",
issue_codes::LINT_RF_002,
);
}
#[test]
fn lint_sqlfluff_parity_rule_smoke_cases() {
let cases = [
("LINT_AL_001", "SELECT * FROM a x JOIN b y ON x.id = y.id"),
("LINT_AL_002", "SELECT a + 1 AS x, b + 2 y FROM t"),
("LINT_AL_004", "SELECT * FROM a t JOIN b t ON t.id = t.id"),
("LINT_AL_008", "SELECT a AS x, b AS x FROM t"),
("LINT_AL_009", "SELECT a AS a FROM t"),
("LINT_AM_009", "SELECT a FROM t LIMIT 10"),
("LINT_AM_003", "SELECT a, b FROM t ORDER BY a, b DESC"),
("LINT_AM_005", "SELECT * FROM a JOIN b ON a.id = b.id"),
("LINT_AM_006", "SELECT foo, bar FROM fake_table GROUP BY 1, bar"),
("LINT_AM_007", "WITH cte AS (SELECT a, b, c FROM t) SELECT * FROM cte UNION SELECT d, e FROM t2"),
("LINT_AM_008", "SELECT foo.a, bar.b FROM foo INNER JOIN bar"),
("LINT_CP_001", "SELECT a from t"),
("LINT_CP_002", "SELECT Col, col FROM t"),
("LINT_CP_003", "SELECT COUNT(*), count(name) FROM t"),
("LINT_CP_004", "SELECT NULL, true FROM t"),
("LINT_CP_005", "CREATE TABLE t (a INT, b varchar(10))"),
("LINT_CV_001", "SELECT * FROM t WHERE a <> b AND c != d"),
("LINT_CV_002", "SELECT IFNULL(a, 0) FROM t"),
("LINT_CV_003", "SELECT a, FROM t"),
("LINT_CV_006", "SELECT 1 ;"),
("LINT_CV_007", "(SELECT 1)"),
("LINT_CV_009", "SELECT foo FROM t"),
("LINT_CV_011", "SELECT CAST(a AS INT)::TEXT FROM t"),
("LINT_CV_012", "SELECT foo.a, bar.b FROM foo JOIN bar WHERE foo.x = bar.y"),
("LINT_JJ_001", "SELECT '{{foo}}' AS templated"),
("LINT_LT_001", "SELECT payload->>'id' FROM t"),
("LINT_LT_002", "SELECT a\n , b\nFROM t"),
("LINT_LT_003", "SELECT a +\n b FROM t"),
("LINT_LT_004", "SELECT a,b FROM t"),
(
"LINT_LT_005",
"SELECT this_is_a_very_long_column_name_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx FROM t",
),
("LINT_LT_006", "SELECT COUNT (1) FROM t"),
("LINT_LT_008", "WITH cte AS (SELECT 1) SELECT * FROM cte"),
("LINT_LT_009", "SELECT a,b,c,d,e FROM t"),
("LINT_LT_010", "SELECT\nDISTINCT a\nFROM t"),
("LINT_LT_011", "SELECT 1 UNION SELECT 2\nUNION SELECT 3"),
("LINT_LT_012", "SELECT 1\nFROM t"),
("LINT_LT_013", "\n\nSELECT 1"),
("LINT_LT_014", "SELECT a FROM t WHERE a = 1 ORDER BY a"),
("LINT_LT_014", "SELECT a FROM t\nWHERE a = 1"),
("LINT_LT_015", "SELECT 1\n\n\nFROM t"),
("LINT_RF_001", "SELECT x.a FROM t"),
("LINT_RF_002", "SELECT id FROM a JOIN b ON a.id = b.id"),
("LINT_RF_003", "SELECT a.id, id2 FROM a"),
("LINT_RF_005", "SELECT \"bad-name\" FROM t"),
("LINT_RF_006", "SELECT \"good_name\" FROM t"),
(
"LINT_ST_002",
"SELECT CASE WHEN x > 0 THEN true ELSE false END FROM t",
),
(
"LINT_ST_004",
"SELECT CASE WHEN species = 'Rat' THEN 'Squeak' ELSE CASE WHEN species = 'Dog' THEN 'Woof' END END FROM t",
),
("LINT_ST_005", "SELECT * FROM t JOIN (SELECT * FROM u) sub ON t.id = sub.id"),
("LINT_ST_006", "SELECT a + 1, a FROM t"),
("LINT_ST_008", "SELECT DISTINCT(a) FROM t"),
("LINT_ST_009", "SELECT * FROM a x JOIN b y ON y.id = x.id"),
("LINT_ST_010", "SELECT * FROM t WHERE col = col"),
("LINT_ST_011", "SELECT a.id FROM a LEFT JOIN b b1 ON a.id = b1.id"),
("LINT_ST_012", "SELECT 1;;"),
("LINT_TQ_001", "CREATE PROCEDURE sp_legacy AS SELECT 1;"),
("LINT_TQ_002", "CREATE PROCEDURE p AS SELECT 1; SELECT 2;"),
];
for (code, sql) in cases {
let issues = if code.starts_with("LINT_TQ_") {
run_lint_in_dialect(sql, Dialect::Mssql)
} else {
run_lint(sql)
};
assert!(
issues.iter().any(|(c, _)| c == code),
"expected {code} for SQL: {sql}; got: {issues:?}"
);
}
let cv10_issues = run_lint_with_config_in_dialect(
"SELECT 'abc', \"def\"",
Dialect::Bigquery,
LintConfig::default(),
);
assert!(
cv10_issues
.iter()
.any(|(code, _)| code == issue_codes::LINT_CV_010),
"expected {} in BigQuery dialect smoke case: {cv10_issues:?}",
issue_codes::LINT_CV_010,
);
let al07_issues = run_lint_with_config(
"SELECT * FROM users u",
LintConfig {
enabled: true,
disabled_rules: vec![],
rule_configs: std::collections::BTreeMap::from([(
"aliasing.forbid".to_string(),
serde_json::json!({"force_enable": true}),
)]),
},
);
assert!(
al07_issues
.iter()
.any(|(code, _)| code == issue_codes::LINT_AL_007),
"expected {} with force_enable=true in smoke case: {al07_issues:?}",
issue_codes::LINT_AL_007,
);
let al06_issues = run_lint_with_config(
"SELECT * FROM a x JOIN b yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy ON x.id = yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy.id",
LintConfig {
enabled: true,
disabled_rules: vec![],
rule_configs: std::collections::BTreeMap::from([(
"aliasing.length".to_string(),
serde_json::json!({"max_alias_length": 30}),
)]),
},
);
assert!(
al06_issues
.iter()
.any(|(code, _)| code == issue_codes::LINT_AL_006),
"expected {} with max_alias_length=30 in smoke case: {al06_issues:?}",
issue_codes::LINT_AL_006,
);
}