use std::process::Command;
use tempfile::tempdir;
const SQL_WITH_VIOLATIONS: &str = "SELECT 1 AS c\nUNION\nSELECT 2 AS c\n";
const SQL_CLEAN: &str = "SELECT 1\n";
const SQL_INVALID: &str = "SELECT FROM";
const SQL_TEMPLATED_ST05: &str = r#"SELECT
a_table.id,
b_table.id
FROM a_table
INNER JOIN (
SELECT
id,
{{"mrgn"}} AS margin
FROM b_tbl
) AS b_table ON a_table.some_column = b_table.some_column"#;
const SQL_COMMENTED_SAFE_FIX: &str =
"-- keep:lead\nSELECT COUNT(1) AS row_count /* keep:mid */\nFROM t -- keep:tail\n";
const SQL_UNSAFE_FIX_REPRESENTATIVE: &str =
"SELECT t.id\nFROM t\nINNER JOIN (\n SELECT id\n FROM u\n) AS u2 ON t.id = u2.id\n";
fn run_flowscope(args: &[&str]) -> std::process::Output {
Command::new(env!("CARGO_BIN_EXE_flowscope"))
.args(args)
.output()
.expect("run CLI")
}
fn combined_output(output: &std::process::Output) -> String {
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
format!("{stdout}\n{stderr}")
}
fn contains_any(haystack: &str, needles: &[&str]) -> bool {
let lowercase = haystack.to_ascii_lowercase();
needles
.iter()
.any(|needle| lowercase.contains(&needle.to_ascii_lowercase()))
}
fn assert_flag_was_accepted(output: &std::process::Output, flag: &str) {
let stderr = String::from_utf8_lossy(&output.stderr).to_ascii_lowercase();
assert_ne!(
output.status.code(),
Some(2),
"Expected {flag} to be accepted by the CLI, got clap parse failure: {stderr}"
);
assert!(
!stderr.contains("unexpected argument"),
"Expected {flag} to be accepted, but CLI reported an unexpected argument: {stderr}"
);
}
fn lint_violation_codes(sql_path: &std::path::Path) -> Vec<String> {
let output = run_flowscope(&[
"--lint",
"--format",
"json",
sql_path.to_str().expect("sql path"),
]);
let stdout = String::from_utf8_lossy(&output.stdout);
let parsed: serde_json::Value = serde_json::from_str(&stdout).unwrap_or_else(|err| {
panic!("Expected valid lint JSON output, got error: {err}; stdout: {stdout}")
});
let files = parsed.as_array().expect("Expected top-level JSON array");
assert_eq!(files.len(), 1, "Expected exactly one file result: {stdout}");
files[0]["violations"]
.as_array()
.expect("Expected violations array")
.iter()
.map(|violation| {
violation["code"]
.as_str()
.unwrap_or("<unknown>")
.to_string()
})
.collect()
}
#[test]
fn test_lint_clean_file() {
let dir = tempdir().expect("temp dir");
let sql_path = dir.path().join("clean.sql");
std::fs::write(&sql_path, SQL_CLEAN).expect("write sql");
let output = Command::new(env!("CARGO_BIN_EXE_flowscope"))
.args(["--lint", sql_path.to_str().expect("sql path")])
.output()
.expect("run CLI");
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(output.status.success(), "Expected exit 0, got: {stdout}");
assert!(stdout.contains("PASS"), "Expected PASS in output: {stdout}");
assert!(
stdout.contains("0 violations"),
"Expected 0 violations: {stdout}"
);
}
#[test]
fn test_lint_file_with_violations() {
let dir = tempdir().expect("temp dir");
let sql_path = dir.path().join("bad.sql");
std::fs::write(&sql_path, SQL_WITH_VIOLATIONS).expect("write sql");
let output = Command::new(env!("CARGO_BIN_EXE_flowscope"))
.args(["--lint", sql_path.to_str().expect("sql path")])
.output()
.expect("run CLI");
let stdout = String::from_utf8_lossy(&output.stdout);
assert_eq!(
output.status.code(),
Some(1),
"Expected exit 1, got: {stdout}"
);
assert!(stdout.contains("FAIL"), "Expected FAIL in output: {stdout}");
assert!(stdout.contains("AM02"), "Expected AM02: {stdout}");
assert!(
stdout.contains("1 violations"),
"Expected 1 violation: {stdout}"
);
}
#[test]
fn test_lint_invalid_sql_fails() {
let dir = tempdir().expect("temp dir");
let sql_path = dir.path().join("invalid.sql");
std::fs::write(&sql_path, SQL_INVALID).expect("write sql");
let output = Command::new(env!("CARGO_BIN_EXE_flowscope"))
.args(["--lint", sql_path.to_str().expect("sql path")])
.output()
.expect("run CLI");
let stdout = String::from_utf8_lossy(&output.stdout);
assert_eq!(
output.status.code(),
Some(1),
"Expected exit 1 for invalid SQL, got: {stdout}"
);
assert!(stdout.contains("FAIL"), "Expected FAIL in output: {stdout}");
assert!(
stdout.contains("1 file failed"),
"Expected failed summary for invalid SQL: {stdout}"
);
}
#[test]
fn test_lint_exclude_rules() {
let dir = tempdir().expect("temp dir");
let sql_path = dir.path().join("excluded.sql");
std::fs::write(&sql_path, SQL_WITH_VIOLATIONS).expect("write sql");
let output = Command::new(env!("CARGO_BIN_EXE_flowscope"))
.args([
"--lint",
"--exclude-rules",
"LINT_AM_002",
sql_path.to_str().expect("sql path"),
])
.output()
.expect("run CLI");
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
output.status.success(),
"Expected exit 0 when rule excluded, got: {stdout}"
);
assert!(
stdout.contains("PASS"),
"Expected PASS when rule excluded: {stdout}"
);
}
#[test]
fn test_lint_fix_respects_exclude_rules() {
let dir = tempdir().expect("temp dir");
let sql_path = dir.path().join("excluded_fix.sql");
std::fs::write(&sql_path, SQL_WITH_VIOLATIONS).expect("write sql");
let output = Command::new(env!("CARGO_BIN_EXE_flowscope"))
.args([
"--lint",
"--fix",
"--exclude-rules",
"LINT_AM_002",
sql_path.to_str().expect("sql path"),
])
.output()
.expect("run CLI");
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
output.status.success(),
"Expected exit 0 when excluded rule is the only violation, got: {stdout}"
);
let after = std::fs::read_to_string(&sql_path).expect("read SQL after fix");
assert_eq!(
after, SQL_WITH_VIOLATIONS,
"Expected file to remain unchanged when fix rule is excluded"
);
}
#[test]
fn test_lint_fix_excluded_rule_not_rewritten_when_other_fixes_apply() {
let dir = tempdir().expect("temp dir");
let sql_path = dir.path().join("excluded_mixed_fix.sql");
let sql = "SELECT COUNT(1) FROM t WHERE a<>b";
std::fs::write(&sql_path, sql).expect("write sql");
let output = Command::new(env!("CARGO_BIN_EXE_flowscope"))
.args([
"--lint",
"--fix",
"--exclude-rules",
"LINT_CV_001",
sql_path.to_str().expect("sql path"),
])
.output()
.expect("run CLI");
assert_eq!(
output.status.code(),
Some(1),
"Expected exit 1 due remaining non-excluded violations"
);
let after = std::fs::read_to_string(&sql_path).expect("read SQL after fix");
assert!(
after.to_ascii_uppercase().contains("COUNT(*)"),
"Expected non-excluded fix to apply: {after}"
);
assert!(
after.contains("<>"),
"Expected excluded CV_005 to remain '<>' (not '!='): {after}"
);
assert!(
!after.contains("!="),
"Expected excluded CV_005 to avoid not-equal rewrite: {after}"
);
}
#[test]
fn test_lint_output_file_has_no_ansi_sequences() {
let dir = tempdir().expect("temp dir");
let sql_path = dir.path().join("bad.sql");
let report_path = dir.path().join("lint.txt");
std::fs::write(&sql_path, SQL_WITH_VIOLATIONS).expect("write sql");
let output = Command::new(env!("CARGO_BIN_EXE_flowscope"))
.args([
"--lint",
"--output",
report_path.to_str().expect("report path"),
sql_path.to_str().expect("sql path"),
])
.output()
.expect("run CLI");
assert_eq!(
output.status.code(),
Some(1),
"Expected exit 1 for violations"
);
let report = std::fs::read_to_string(report_path).expect("read lint report");
assert!(
!report.contains('\u{1b}'),
"Expected no ANSI escape sequences in output file: {report}"
);
}
#[test]
fn test_lint_json_format() {
let dir = tempdir().expect("temp dir");
let sql_path = dir.path().join("json.sql");
std::fs::write(&sql_path, SQL_WITH_VIOLATIONS).expect("write sql");
let output = Command::new(env!("CARGO_BIN_EXE_flowscope"))
.args([
"--lint",
"--format",
"json",
sql_path.to_str().expect("sql path"),
])
.output()
.expect("run CLI");
let stdout = String::from_utf8_lossy(&output.stdout);
assert_eq!(
output.status.code(),
Some(1),
"Expected exit 1 for violations: {stdout}"
);
let parsed: serde_json::Value =
serde_json::from_str(&stdout).expect("Expected valid JSON output");
let arr = parsed.as_array().expect("Expected JSON array");
assert_eq!(arr.len(), 1);
assert!(!arr[0]["violations"].as_array().unwrap().is_empty());
}
#[test]
fn test_lint_unsupported_format() {
let dir = tempdir().expect("temp dir");
let sql_path = dir.path().join("test.sql");
std::fs::write(&sql_path, SQL_CLEAN).expect("write sql");
let output = Command::new(env!("CARGO_BIN_EXE_flowscope"))
.args([
"--lint",
"--format",
"html",
sql_path.to_str().expect("sql path"),
])
.output()
.expect("run CLI");
assert_eq!(
output.status.code(),
Some(66),
"Expected exit 66 for unsupported format"
);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.contains("only supports"),
"Expected helpful error message: {stderr}"
);
}
#[test]
fn test_lint_unsupported_format_fails_before_fix_mutation() {
let dir = tempdir().expect("temp dir");
let sql_path = dir.path().join("test.sql");
std::fs::write(&sql_path, SQL_WITH_VIOLATIONS).expect("write sql");
let output = Command::new(env!("CARGO_BIN_EXE_flowscope"))
.args([
"--lint",
"--fix",
"--format",
"html",
sql_path.to_str().expect("sql path"),
])
.output()
.expect("run CLI");
assert_eq!(
output.status.code(),
Some(66),
"Expected exit 66 for unsupported lint format"
);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.contains("only supports"),
"Expected helpful error message: {stderr}"
);
let after = std::fs::read_to_string(&sql_path).expect("read SQL after failed command");
assert_eq!(
after, SQL_WITH_VIOLATIONS,
"Expected file to remain unchanged for unsupported format"
);
}
#[test]
fn test_lint_stdin() {
let output = Command::new(env!("CARGO_BIN_EXE_flowscope"))
.args(["--lint"])
.stdin(std::process::Stdio::piped())
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.spawn()
.and_then(|mut child| {
use std::io::Write;
child
.stdin
.take()
.unwrap()
.write_all(SQL_WITH_VIOLATIONS.as_bytes())
.unwrap();
child.wait_with_output()
})
.expect("run CLI");
let stdout = String::from_utf8_lossy(&output.stdout);
assert_eq!(
output.status.code(),
Some(1),
"Expected exit 1 for stdin violations: {stdout}"
);
assert!(
stdout.contains("AM02"),
"Expected AM02 from stdin: {stdout}"
);
}
#[test]
fn test_lint_fix_only_skips_post_fix_lint_for_file_inputs() {
let dir = tempdir().expect("temp dir");
let sql_path = dir.path().join("fix_only.sql");
std::fs::write(&sql_path, SQL_WITH_VIOLATIONS).expect("write sql");
let output = Command::new(env!("CARGO_BIN_EXE_flowscope"))
.args([
"--lint",
"--fix",
"--fix-only",
"--format",
"html",
sql_path.to_str().expect("sql path"),
])
.output()
.expect("run CLI");
assert!(
output.status.success(),
"Expected --fix-only to complete successfully"
);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
!stderr.contains("phase 2/2 linting post-fix inputs"),
"Expected --fix-only to skip post-fix lint phase: {stderr}"
);
assert!(
stderr.contains("phase timing: fix="),
"Expected --fix-only timing output to include fix phase: {stderr}"
);
assert!(
String::from_utf8_lossy(&output.stdout).is_empty(),
"Expected no lint report output for file-based --fix-only run"
);
let fixed_sql = std::fs::read_to_string(&sql_path).expect("read fixed sql");
assert!(
fixed_sql.contains("DISTINCT"),
"Expected --fix-only to apply file fixes in place: {fixed_sql}"
);
}
#[test]
fn test_lint_fix_only_emits_fixed_sql_for_stdin() {
let output = Command::new(env!("CARGO_BIN_EXE_flowscope"))
.args(["--lint", "--fix", "--fix-only"])
.stdin(std::process::Stdio::piped())
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.spawn()
.and_then(|mut child| {
use std::io::Write;
child
.stdin
.take()
.unwrap()
.write_all(SQL_WITH_VIOLATIONS.as_bytes())
.unwrap();
child.wait_with_output()
})
.expect("run CLI");
assert!(
output.status.success(),
"Expected --fix-only stdin run to succeed"
);
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
stdout.contains("DISTINCT"),
"Expected --fix-only stdin output to contain fixed SQL: {stdout}"
);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
!stderr.contains("phase 2/2 linting post-fix inputs"),
"Expected --fix-only to skip post-fix lint phase: {stderr}"
);
}
#[test]
fn test_lint_templated_sql_without_template_flag_uses_jinja_fallback() {
let output = Command::new(env!("CARGO_BIN_EXE_flowscope"))
.args(["--lint", "--format", "json"])
.stdin(std::process::Stdio::piped())
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.spawn()
.and_then(|mut child| {
use std::io::Write;
child
.stdin
.take()
.unwrap()
.write_all(SQL_TEMPLATED_ST05.as_bytes())
.unwrap();
child.wait_with_output()
})
.expect("run CLI");
assert_eq!(
output.status.code(),
Some(1),
"Expected violations in fallback lint"
);
let stdout = String::from_utf8_lossy(&output.stdout);
let parsed: serde_json::Value =
serde_json::from_str(&stdout).expect("Expected valid JSON output");
let violations = parsed[0]["violations"]
.as_array()
.expect("violations array");
let has_st05 = violations
.iter()
.any(|v| v["code"].as_str() == Some("ST05"));
let has_parse_error = violations
.iter()
.any(|v| v["code"].as_str() == Some("PARSE_ERROR"));
assert!(
has_st05,
"Expected ST05 violation in templated fallback: {stdout}"
);
assert!(
!has_parse_error,
"Did not expect PARSE_ERROR after Jinja fallback: {stdout}"
);
}
#[test]
fn test_lint_rule_configs_flag_applies_rule_options() {
let dir = tempdir().expect("temp dir");
let sql_path = dir.path().join("subquery.sql");
std::fs::write(&sql_path, "SELECT * FROM (SELECT * FROM t) sub\n").expect("write sql");
let no_cfg = Command::new(env!("CARGO_BIN_EXE_flowscope"))
.args([
"--lint",
"--format",
"json",
sql_path.to_str().expect("sql path"),
])
.output()
.expect("run CLI without rule configs");
let no_cfg_stdout = String::from_utf8_lossy(&no_cfg.stdout);
let no_cfg_json: serde_json::Value =
serde_json::from_str(&no_cfg_stdout).expect("valid json without rule configs");
let no_cfg_has_st05 = no_cfg_json[0]["violations"]
.as_array()
.expect("violations array")
.iter()
.any(|v| v["code"].as_str() == Some("ST05"));
assert!(
!no_cfg_has_st05,
"Expected default ST05 config (join) to ignore FROM subquery: {no_cfg_stdout}"
);
let with_cfg = Command::new(env!("CARGO_BIN_EXE_flowscope"))
.args([
"--lint",
"--format",
"json",
"--rule-configs",
r#"{"structure.subquery":{"forbid_subquery_in":"from"}}"#,
sql_path.to_str().expect("sql path"),
])
.output()
.expect("run CLI with rule configs");
let with_cfg_stdout = String::from_utf8_lossy(&with_cfg.stdout);
let with_cfg_json: serde_json::Value =
serde_json::from_str(&with_cfg_stdout).expect("valid json with rule configs");
let with_cfg_has_st05 = with_cfg_json[0]["violations"]
.as_array()
.expect("violations array")
.iter()
.any(|v| v["code"].as_str() == Some("ST05"));
assert!(
with_cfg_has_st05,
"Expected ST05 with forbid_subquery_in=from: {with_cfg_stdout}"
);
}
#[test]
fn test_lint_fix_rule_configs_enable_cv006_core_autofix_in_patch_mode() {
let dir = tempdir().expect("temp dir");
let sql_path = dir.path().join("missing_final_semicolon.sql");
std::fs::write(&sql_path, "SELECT 1").expect("write sql");
let output = Command::new(env!("CARGO_BIN_EXE_flowscope"))
.args([
"--lint",
"--fix",
"--rule-configs",
r#"{"convention.terminator":{"require_final_semicolon":true}}"#,
sql_path.to_str().expect("sql path"),
])
.output()
.expect("run CLI with fix + rule configs");
assert_ne!(
output.status.code(),
Some(2),
"Expected CLI invocation to succeed: {}",
combined_output(&output)
);
let after = std::fs::read_to_string(&sql_path).expect("read SQL after fix");
assert!(
after.trim_end().ends_with(';'),
"Expected CV06 core autofix to append a final semicolon in patch mode: {after}"
);
}
#[test]
fn test_lint_fix_applies_cv006_spacing_core_autofix_in_patch_mode() {
let dir = tempdir().expect("temp dir");
let sql_path = dir.path().join("terminator_spacing_patch_fix.sql");
std::fs::write(&sql_path, "SELECT a FROM foo ;").expect("write sql");
let output = Command::new(env!("CARGO_BIN_EXE_flowscope"))
.args(["--lint", "--fix", sql_path.to_str().expect("sql path")])
.output()
.expect("run CLI with fix");
assert_ne!(
output.status.code(),
Some(2),
"Expected CLI invocation to succeed: {}",
combined_output(&output)
);
let after = std::fs::read_to_string(&sql_path).expect("read SQL after fix");
assert_eq!(
after, "SELECT a FROM foo;",
"Expected CV06 core autofix to remove whitespace before statement terminator"
);
}
#[test]
fn test_lint_fix_applies_cv002_core_autofix_in_patch_mode() {
let dir = tempdir().expect("temp dir");
let sql_path = dir.path().join("coalesce_patch_fix.sql");
std::fs::write(&sql_path, "SELECT IFNULL(foo, 0) FROM t").expect("write sql");
let output = Command::new(env!("CARGO_BIN_EXE_flowscope"))
.args(["--lint", "--fix", sql_path.to_str().expect("sql path")])
.output()
.expect("run CLI with fix");
assert_ne!(
output.status.code(),
Some(2),
"Expected CLI invocation to succeed: {}",
combined_output(&output)
);
let after = std::fs::read_to_string(&sql_path).expect("read SQL after fix");
assert!(
after.to_ascii_uppercase().contains("COALESCE"),
"Expected CV02 core autofix to rewrite IFNULL to COALESCE in patch mode: {after}"
);
}
#[test]
fn test_lint_fix_applies_cv001_core_autofix_in_patch_mode() {
let dir = tempdir().expect("temp dir");
let sql_path = dir.path().join("not_equal_style_patch_fix.sql");
std::fs::write(&sql_path, "SELECT * FROM t WHERE a <> b AND c != d").expect("write sql");
let output = Command::new(env!("CARGO_BIN_EXE_flowscope"))
.args(["--lint", "--fix", sql_path.to_str().expect("sql path")])
.output()
.expect("run CLI with fix");
assert_ne!(
output.status.code(),
Some(2),
"Expected CLI invocation to succeed: {}",
combined_output(&output)
);
let after = std::fs::read_to_string(&sql_path).expect("read SQL after fix");
let compact: String = after.chars().filter(|ch| !ch.is_whitespace()).collect();
let has_c_style = compact.contains("a!=b") && compact.contains("c!=d");
let has_ansi_style = compact.contains("a<>b") && compact.contains("c<>d");
assert!(
has_c_style || has_ansi_style,
"Expected CV01 core autofix to normalize not-equal operators to one style: {after}"
);
}
#[test]
fn test_lint_fix_applies_cv005_core_autofix_in_patch_mode() {
let dir = tempdir().expect("temp dir");
let sql_path = dir.path().join("null_comparison_patch_fix.sql");
std::fs::write(&sql_path, "SELECT * FROM t WHERE a = NULL AND b <> NULL").expect("write sql");
let output = Command::new(env!("CARGO_BIN_EXE_flowscope"))
.args(["--lint", "--fix", sql_path.to_str().expect("sql path")])
.output()
.expect("run CLI with fix");
assert_ne!(
output.status.code(),
Some(2),
"Expected CLI invocation to succeed: {}",
combined_output(&output)
);
let after = std::fs::read_to_string(&sql_path).expect("read SQL after fix");
assert!(
after.contains("a IS NULL"),
"Expected CV05 core autofix to rewrite '= NULL' to 'IS NULL': {after}"
);
assert!(
after.contains("b IS NOT NULL"),
"Expected CV05 core autofix to rewrite '<> NULL' to 'IS NOT NULL': {after}"
);
}
#[test]
fn test_lint_fix_applies_cv007_core_autofix_in_patch_mode() {
let dir = tempdir().expect("temp dir");
let sql_path = dir.path().join("statement_brackets_patch_fix.sql");
std::fs::write(&sql_path, "(SELECT 1)").expect("write sql");
let output = Command::new(env!("CARGO_BIN_EXE_flowscope"))
.args(["--lint", "--fix", sql_path.to_str().expect("sql path")])
.output()
.expect("run CLI with fix");
assert_ne!(
output.status.code(),
Some(2),
"Expected CLI invocation to succeed: {}",
combined_output(&output)
);
let after = std::fs::read_to_string(&sql_path).expect("read SQL after fix");
assert_eq!(
after, "SELECT 1\n",
"Expected CV07 core autofix to remove outer wrapper brackets in patch mode"
);
}
#[test]
fn test_lint_fix_applies_lt006_core_autofix_in_patch_mode() {
let dir = tempdir().expect("temp dir");
let sql_path = dir.path().join("function_spacing_patch_fix.sql");
std::fs::write(&sql_path, "SELECT COUNT (1) FROM t").expect("write sql");
let output = Command::new(env!("CARGO_BIN_EXE_flowscope"))
.args(["--lint", "--fix", sql_path.to_str().expect("sql path")])
.output()
.expect("run CLI with fix");
assert_ne!(
output.status.code(),
Some(2),
"Expected CLI invocation to succeed: {}",
combined_output(&output)
);
let after = std::fs::read_to_string(&sql_path).expect("read SQL after fix");
assert!(
after.contains("COUNT("),
"Expected LT06 core autofix to keep function call parenthesis tight: {after}"
);
assert!(
!after.contains("COUNT ("),
"Expected LT06 core autofix to remove spacing before function parenthesis: {after}"
);
}
#[test]
fn test_lint_fix_applies_lt005_core_autofix_in_patch_mode_with_config() {
let dir = tempdir().expect("temp dir");
let sql_path = dir.path().join("long_line_patch_fix.sql");
let sql = format!("SELECT {} FROM t\n", vec!["column_name"; 60].join(" "));
std::fs::write(&sql_path, sql).expect("write sql");
let output = Command::new(env!("CARGO_BIN_EXE_flowscope"))
.args([
"--lint",
"--fix",
"--rule-configs",
r#"{"layout.long_lines":{"max_line_length":300}}"#,
sql_path.to_str().expect("sql path"),
])
.output()
.expect("run CLI with fix");
assert_ne!(
output.status.code(),
Some(2),
"Expected CLI invocation to succeed: {}",
combined_output(&output)
);
let after = std::fs::read_to_string(&sql_path).expect("read SQL after fix");
assert!(
after.lines().all(|line| line.len() <= 300),
"Expected LT005 core autofix output lines to stay under configured threshold: {after:?}"
);
assert!(
after.lines().count() > 1,
"Expected LT005 core autofix to split an extremely long line: {after:?}"
);
}
#[test]
fn test_lint_fix_applies_al005_core_autofix_in_patch_mode() {
let dir = tempdir().expect("temp dir");
let sql_path = dir.path().join("unused_table_alias_patch_fix.sql");
std::fs::write(
&sql_path,
"SELECT users.name FROM users AS u JOIN orders AS o ON users.id = orders.user_id\n",
)
.expect("write sql");
let output = Command::new(env!("CARGO_BIN_EXE_flowscope"))
.args([
"--lint",
"--fix",
"--exclude-rules",
"LINT_AM_005",
sql_path.to_str().expect("sql path"),
])
.output()
.expect("run CLI with fix");
assert_ne!(
output.status.code(),
Some(2),
"Expected CLI invocation to succeed: {}",
combined_output(&output)
);
let after = std::fs::read_to_string(&sql_path).expect("read SQL after fix");
assert_eq!(
after, "SELECT users.name FROM users JOIN orders ON users.id = orders.user_id\n",
"Expected AL005 core autofix to remove unused table aliases: {after:?}"
);
}
#[test]
fn test_lint_fix_applies_al001_core_autofix_in_patch_mode() {
let dir = tempdir().expect("temp dir");
let sql_path = dir.path().join("aliasing_table_style_patch_fix.sql");
std::fs::write(&sql_path, "SELECT u.id FROM users u\n").expect("write sql");
let output = Command::new(env!("CARGO_BIN_EXE_flowscope"))
.args(["--lint", "--fix", sql_path.to_str().expect("sql path")])
.output()
.expect("run CLI with fix");
assert_ne!(
output.status.code(),
Some(2),
"Expected CLI invocation to succeed: {}",
combined_output(&output)
);
let after = std::fs::read_to_string(&sql_path).expect("read SQL after fix");
assert_eq!(
after, "SELECT u.id FROM users AS u\n",
"Expected AL001 core autofix to insert explicit AS for table aliases: {after:?}"
);
}
#[test]
fn test_lint_fix_applies_al009_core_autofix_in_patch_mode() {
let dir = tempdir().expect("temp dir");
let sql_path = dir.path().join("self_alias_column_patch_fix.sql");
std::fs::write(&sql_path, "SELECT a AS a FROM t\n").expect("write sql");
let output = Command::new(env!("CARGO_BIN_EXE_flowscope"))
.args(["--lint", "--fix", sql_path.to_str().expect("sql path")])
.output()
.expect("run CLI with fix");
assert_ne!(
output.status.code(),
Some(2),
"Expected CLI invocation to succeed: {}",
combined_output(&output)
);
let after = std::fs::read_to_string(&sql_path).expect("read SQL after fix");
assert_eq!(
after, "SELECT a FROM t\n",
"Expected AL009 core autofix to remove self-aliasing projection alias: {after:?}"
);
}
#[test]
fn test_lint_fix_applies_st001_core_autofix_in_patch_mode() {
let dir = tempdir().expect("temp dir");
let sql_path = dir.path().join("else_null_patch_fix.sql");
std::fs::write(
&sql_path,
"SELECT CASE WHEN x > 1 THEN 'a' ELSE NULL END FROM t\n",
)
.expect("write sql");
let output = Command::new(env!("CARGO_BIN_EXE_flowscope"))
.args(["--lint", "--fix", sql_path.to_str().expect("sql path")])
.output()
.expect("run CLI with fix");
assert_ne!(
output.status.code(),
Some(2),
"Expected CLI invocation to succeed: {}",
combined_output(&output)
);
let after = std::fs::read_to_string(&sql_path).expect("read SQL after fix");
assert_eq!(
after, "SELECT CASE WHEN x > 1 THEN 'a' END FROM t\n",
"Expected ST001 core autofix to remove redundant ELSE NULL branch: {after:?}"
);
}
#[test]
fn test_lint_fix_applies_am001_core_autofix_in_patch_mode() {
let dir = tempdir().expect("temp dir");
let sql_path = dir.path().join("distinct_group_by_patch_fix.sql");
std::fs::write(&sql_path, "SELECT DISTINCT a FROM t GROUP BY a\n").expect("write sql");
let output = Command::new(env!("CARGO_BIN_EXE_flowscope"))
.args([
"--lint",
"--fix",
"--exclude-rules",
"LINT_LT_014",
sql_path.to_str().expect("sql path"),
])
.output()
.expect("run CLI with fix");
assert_ne!(
output.status.code(),
Some(2),
"Expected CLI invocation to succeed: {}",
combined_output(&output)
);
let after = std::fs::read_to_string(&sql_path).expect("read SQL after fix");
assert_eq!(
after, "SELECT a FROM t GROUP BY a\n",
"Expected AM001 core autofix to remove DISTINCT when GROUP BY is present: {after:?}"
);
}
#[test]
fn test_lint_fix_applies_am002_core_autofix_in_patch_mode() {
let dir = tempdir().expect("temp dir");
let sql_path = dir.path().join("bare_union_patch_fix.sql");
std::fs::write(&sql_path, "SELECT 1 UNION SELECT 2\n").expect("write sql");
let output = Command::new(env!("CARGO_BIN_EXE_flowscope"))
.args([
"--lint",
"--fix",
"--exclude-rules",
"LINT_LT_011",
sql_path.to_str().expect("sql path"),
])
.output()
.expect("run CLI with fix");
assert_ne!(
output.status.code(),
Some(2),
"Expected CLI invocation to succeed: {}",
combined_output(&output)
);
let after = std::fs::read_to_string(&sql_path).expect("read SQL after fix");
assert_eq!(
after, "SELECT 1 UNION DISTINCT SELECT 2\n",
"Expected AM002 core autofix to expand bare UNION to explicit UNION DISTINCT: {after:?}"
);
}
#[test]
fn test_lint_fix_applies_am003_core_autofix_in_patch_mode() {
let dir = tempdir().expect("temp dir");
let sql_path = dir.path().join("ambiguous_order_by_patch_fix.sql");
std::fs::write(&sql_path, "SELECT * FROM t ORDER BY a DESC, b NULLS LAST\n")
.expect("write sql");
let output = Command::new(env!("CARGO_BIN_EXE_flowscope"))
.args([
"--lint",
"--fix",
"--exclude-rules",
"LINT_LT_014",
sql_path.to_str().expect("sql path"),
])
.output()
.expect("run CLI with fix");
assert_ne!(
output.status.code(),
Some(2),
"Expected CLI invocation to succeed: {}",
combined_output(&output)
);
let after = std::fs::read_to_string(&sql_path).expect("read SQL after fix");
assert_eq!(
after, "SELECT * FROM t ORDER BY a DESC, b ASC NULLS LAST\n",
"Expected AM003 core autofix to add ASC to implicit ORDER BY terms in mixed clauses: {after:?}"
);
}
#[test]
fn test_lint_fix_applies_am005_core_autofix_in_patch_mode() {
let dir = tempdir().expect("temp dir");
let sql_path = dir.path().join("ambiguous_join_patch_fix.sql");
std::fs::write(&sql_path, "SELECT a FROM t JOIN u ON t.id = u.id\n").expect("write sql");
let output = Command::new(env!("CARGO_BIN_EXE_flowscope"))
.args(["--lint", "--fix", sql_path.to_str().expect("sql path")])
.output()
.expect("run CLI with fix");
assert_ne!(
output.status.code(),
Some(2),
"Expected CLI invocation to succeed: {}",
combined_output(&output)
);
let after = std::fs::read_to_string(&sql_path).expect("read SQL after fix");
assert_eq!(
after, "SELECT a FROM t INNER JOIN u ON t.id = u.id\n",
"Expected AM005 core autofix to qualify bare JOIN with INNER: {after:?}"
);
}
#[test]
fn test_lint_fix_applies_am008_core_autofix_in_patch_mode() {
let dir = tempdir().expect("temp dir");
let sql_path = dir.path().join("ambiguous_join_condition_patch_fix.sql");
std::fs::write(&sql_path, "SELECT foo.a, bar.b FROM foo INNER JOIN bar\n").expect("write sql");
let output = Command::new(env!("CARGO_BIN_EXE_flowscope"))
.args(["--lint", "--fix", sql_path.to_str().expect("sql path")])
.output()
.expect("run CLI with fix");
assert_ne!(
output.status.code(),
Some(2),
"Expected CLI invocation to succeed: {}",
combined_output(&output)
);
let after = std::fs::read_to_string(&sql_path).expect("read SQL after fix");
assert_eq!(
after, "SELECT\n foo.a,\n bar.b\nFROM foo CROSS JOIN bar\n",
"Expected AM008 core autofix to rewrite conditionless INNER JOIN to CROSS JOIN: {after:?}"
);
}
#[test]
fn test_lint_fix_applies_st006_core_autofix_in_patch_mode() {
let dir = tempdir().expect("temp dir");
let sql_path = dir.path().join("structure_column_order_patch_fix.sql");
std::fs::write(&sql_path, "SELECT a + 1, a FROM t\n").expect("write sql");
let output = Command::new(env!("CARGO_BIN_EXE_flowscope"))
.args(["--lint", "--fix", sql_path.to_str().expect("sql path")])
.output()
.expect("run CLI with fix");
assert_ne!(
output.status.code(),
Some(2),
"Expected CLI invocation to succeed: {}",
combined_output(&output)
);
let after = std::fs::read_to_string(&sql_path).expect("read SQL after fix");
assert_eq!(
after, "SELECT\n a,\n a + 1\nFROM t\n",
"Expected ST006 core autofix to reorder simple projection targets ahead of complex expressions: {after:?}"
);
}
#[test]
fn test_lint_fix_applies_st009_core_autofix_in_patch_mode() {
let dir = tempdir().expect("temp dir");
let sql_path = dir
.path()
.join("structure_join_condition_order_patch_fix.sql");
std::fs::write(
&sql_path,
"SELECT foo.a, bar.b FROM foo LEFT JOIN bar ON bar.a = foo.a\n",
)
.expect("write sql");
let output = Command::new(env!("CARGO_BIN_EXE_flowscope"))
.args(["--lint", "--fix", sql_path.to_str().expect("sql path")])
.output()
.expect("run CLI with fix");
assert_ne!(
output.status.code(),
Some(2),
"Expected CLI invocation to succeed: {}",
combined_output(&output)
);
let after = std::fs::read_to_string(&sql_path).expect("read SQL after fix");
assert_eq!(
after, "SELECT\n foo.a,\n bar.b\nFROM foo LEFT JOIN bar ON foo.a = bar.a\n",
"Expected ST009 core autofix to reorder join predicate source sides: {after:?}"
);
}
#[test]
fn test_lint_fix_applies_st002_core_autofix_in_patch_mode() {
let dir = tempdir().expect("temp dir");
let sql_path = dir.path().join("structure_simple_case_patch_fix.sql");
std::fs::write(
&sql_path,
"SELECT CASE WHEN x > 0 THEN true ELSE false END FROM t\n",
)
.expect("write sql");
let output = Command::new(env!("CARGO_BIN_EXE_flowscope"))
.args(["--lint", "--fix", sql_path.to_str().expect("sql path")])
.output()
.expect("run CLI with fix");
assert_ne!(
output.status.code(),
Some(2),
"Expected CLI invocation to succeed: {}",
combined_output(&output)
);
let after = std::fs::read_to_string(&sql_path).expect("read SQL after fix");
assert_eq!(
after, "SELECT coalesce(x > 0, false) FROM t\n",
"Expected ST002 core autofix to rewrite unnecessary CASE to coalesce: {after:?}"
);
}
#[test]
fn test_lint_fix_applies_st008_core_autofix_in_patch_mode() {
let dir = tempdir().expect("temp dir");
let sql_path = dir.path().join("distinct_parenthesized_patch_fix.sql");
std::fs::write(&sql_path, "SELECT DISTINCT(a) FROM t\n").expect("write sql");
let output = Command::new(env!("CARGO_BIN_EXE_flowscope"))
.args(["--lint", "--fix", sql_path.to_str().expect("sql path")])
.output()
.expect("run CLI with fix");
assert_ne!(
output.status.code(),
Some(2),
"Expected CLI invocation to succeed: {}",
combined_output(&output)
);
let after = std::fs::read_to_string(&sql_path).expect("read SQL after fix");
assert_eq!(
after, "SELECT DISTINCT a FROM t\n",
"Expected ST008 core autofix to remove DISTINCT parentheses: {after:?}"
);
}
#[test]
fn test_lint_fix_applies_lt004_core_autofix_in_patch_mode() {
let dir = tempdir().expect("temp dir");
let sql_path = dir.path().join("comma_spacing_patch_fix.sql");
std::fs::write(&sql_path, "SELECT a,b FROM t\n").expect("write sql");
let output = Command::new(env!("CARGO_BIN_EXE_flowscope"))
.args(["--lint", "--fix", sql_path.to_str().expect("sql path")])
.output()
.expect("run CLI with fix");
assert_ne!(
output.status.code(),
Some(2),
"Expected CLI invocation to succeed: {}",
combined_output(&output)
);
let after = std::fs::read_to_string(&sql_path).expect("read SQL after fix");
assert!(
!after.contains("a,b"),
"Expected LT004 core autofix to enforce spacing after comma: {after:?}"
);
}
#[test]
fn test_lint_fix_applies_lt003_core_autofix_in_patch_mode() {
let dir = tempdir().expect("temp dir");
let sql_path = dir.path().join("operator_layout_patch_fix.sql");
std::fs::write(&sql_path, "SELECT a +\n b FROM t\n").expect("write sql");
let output = Command::new(env!("CARGO_BIN_EXE_flowscope"))
.args(["--lint", "--fix", sql_path.to_str().expect("sql path")])
.output()
.expect("run CLI with fix");
assert_ne!(
output.status.code(),
Some(2),
"Expected CLI invocation to succeed: {}",
combined_output(&output)
);
let after = std::fs::read_to_string(&sql_path).expect("read SQL after fix");
assert_eq!(
after, "SELECT a\n + b FROM t\n",
"Expected LT003 core autofix to move trailing operator to leading style: {after:?}"
);
}
#[test]
fn test_lint_fix_applies_lt001_core_autofix_in_patch_mode() {
let dir = tempdir().expect("temp dir");
let sql_path = dir.path().join("layout_spacing_patch_fix.sql");
std::fs::write(&sql_path, "SELECT payload->>'id' FROM t\n").expect("write sql");
let output = Command::new(env!("CARGO_BIN_EXE_flowscope"))
.args(["--lint", "--fix", sql_path.to_str().expect("sql path")])
.output()
.expect("run CLI with fix");
assert_ne!(
output.status.code(),
Some(2),
"Expected CLI invocation to succeed: {}",
combined_output(&output)
);
let after = std::fs::read_to_string(&sql_path).expect("read SQL after fix");
assert_eq!(
after, "SELECT payload ->> 'id' FROM t\n",
"Expected LT001 core autofix to normalize json arrow spacing: {after:?}"
);
}
#[test]
fn test_lint_fix_applies_lt002_core_autofix_in_patch_mode() {
let dir = tempdir().expect("temp dir");
let sql_path = dir.path().join("layout_indent_patch_fix.sql");
std::fs::write(&sql_path, "SELECT 1\n -- comment\nFROM t\n").expect("write sql");
let output = Command::new(env!("CARGO_BIN_EXE_flowscope"))
.args(["--lint", "--fix", sql_path.to_str().expect("sql path")])
.output()
.expect("run CLI with fix");
assert_ne!(
output.status.code(),
Some(2),
"Expected CLI invocation to succeed: {}",
combined_output(&output)
);
let after = std::fs::read_to_string(&sql_path).expect("read SQL after fix");
assert_eq!(
after, "SELECT 1\n -- comment\nFROM t\n",
"Expected LT002 core autofix to normalize comment-line indentation: {after:?}"
);
}
#[test]
fn test_lint_fix_applies_tq003_core_autofix_in_patch_mode() {
let dir = tempdir().expect("temp dir");
let sql_path = dir.path().join("tsql_empty_batch_patch_fix.sql");
std::fs::write(&sql_path, "SELECT 1\nGO\nGO\nSELECT 2\n").expect("write sql");
let output = Command::new(env!("CARGO_BIN_EXE_flowscope"))
.args([
"--dialect",
"mssql",
"--lint",
"--fix",
sql_path.to_str().expect("sql path"),
])
.output()
.expect("run CLI with fix");
assert_ne!(
output.status.code(),
Some(2),
"Expected CLI invocation to succeed: {}",
combined_output(&output)
);
let after = std::fs::read_to_string(&sql_path).expect("read SQL after fix");
assert_eq!(
after, "SELECT 1\nGO\nSELECT 2\n",
"Expected TQ003 core autofix to collapse redundant GO batch separators: {after:?}"
);
}
#[test]
fn test_lint_fix_applies_tq002_core_autofix_in_patch_mode() {
let dir = tempdir().expect("temp dir");
let sql_path = dir.path().join("tsql_procedure_begin_end_patch_fix.sql");
std::fs::write(&sql_path, "CREATE PROCEDURE p AS SELECT 1; SELECT 2;\n").expect("write sql");
let output = Command::new(env!("CARGO_BIN_EXE_flowscope"))
.args([
"--dialect",
"mssql",
"--lint",
"--fix",
sql_path.to_str().expect("sql path"),
])
.output()
.expect("run CLI with fix");
assert_ne!(
output.status.code(),
Some(2),
"Expected CLI invocation to succeed: {}",
combined_output(&output)
);
let after = std::fs::read_to_string(&sql_path).expect("read SQL after fix");
assert!(
after.to_ascii_uppercase().contains(" AS BEGIN "),
"Expected TQ002 core autofix to insert BEGIN: {after:?}"
);
assert!(
after.to_ascii_uppercase().contains(" END"),
"Expected TQ002 core autofix to insert END: {after:?}"
);
}
#[test]
fn test_lint_fix_applies_st005_core_autofix_in_unsafe_mode_with_from_config() {
let dir = tempdir().expect("temp dir");
let sql_path = dir.path().join("st005_from_subquery_core_patch_fix.sql");
std::fs::write(&sql_path, "SELECT * FROM (SELECT 1) sub\n").expect("write sql");
let output = Command::new(env!("CARGO_BIN_EXE_flowscope"))
.args([
"--lint",
"--fix",
"--unsafe-fixes",
"--rule-configs",
r#"{"structure.subquery":{"forbid_subquery_in":"from"}}"#,
sql_path.to_str().expect("sql path"),
])
.output()
.expect("run CLI with unsafe fix");
assert_ne!(
output.status.code(),
Some(2),
"Expected CLI invocation to succeed: {}",
combined_output(&output)
);
let after = std::fs::read_to_string(&sql_path).expect("read SQL after fix");
assert_eq!(
after, "WITH sub AS (SELECT 1)\n\nSELECT * FROM sub\n",
"Expected unsafe ST005 core autofix to rewrite FROM subquery to CTE: {after:?}"
);
}
#[test]
fn test_lint_fix_applies_cp001_core_autofix_in_patch_mode() {
let dir = tempdir().expect("temp dir");
let sql_path = dir.path().join("keyword_capitalisation_patch_fix.sql");
std::fs::write(&sql_path, "SELECT a from t\n").expect("write sql");
let output = Command::new(env!("CARGO_BIN_EXE_flowscope"))
.args(["--lint", "--fix", sql_path.to_str().expect("sql path")])
.output()
.expect("run CLI with fix");
assert_ne!(
output.status.code(),
Some(2),
"Expected CLI invocation to succeed: {}",
combined_output(&output)
);
let after = std::fs::read_to_string(&sql_path).expect("read SQL after fix");
assert_eq!(
after, "SELECT a FROM t\n",
"Expected CP001 core autofix to normalize keyword capitalisation: {after:?}"
);
}
#[test]
fn test_lint_fix_applies_cp003_core_autofix_in_patch_mode() {
let dir = tempdir().expect("temp dir");
let sql_path = dir.path().join("function_capitalisation_patch_fix.sql");
std::fs::write(&sql_path, "SELECT COUNT(*), sum(a) FROM t\n").expect("write sql");
let output = Command::new(env!("CARGO_BIN_EXE_flowscope"))
.args(["--lint", "--fix", sql_path.to_str().expect("sql path")])
.output()
.expect("run CLI with fix");
assert_ne!(
output.status.code(),
Some(2),
"Expected CLI invocation to succeed: {}",
combined_output(&output)
);
let after = std::fs::read_to_string(&sql_path).expect("read SQL after fix");
assert_eq!(
after, "SELECT\n COUNT(*),\n SUM(a)\nFROM t\n",
"Expected CP003 core autofix to normalize function capitalisation: {after:?}"
);
}
#[test]
fn test_lint_fix_applies_cp002_core_autofix_in_patch_mode() {
let dir = tempdir().expect("temp dir");
let sql_path = dir.path().join("identifier_capitalisation_patch_fix.sql");
std::fs::write(&sql_path, "SELECT Col, col FROM t\n").expect("write sql");
let output = Command::new(env!("CARGO_BIN_EXE_flowscope"))
.args(["--lint", "--fix", sql_path.to_str().expect("sql path")])
.output()
.expect("run CLI with fix");
assert_ne!(
output.status.code(),
Some(2),
"Expected CLI invocation to succeed: {}",
combined_output(&output)
);
let after = std::fs::read_to_string(&sql_path).expect("read SQL after fix");
assert_eq!(
after, "SELECT\n Col,\n Col\nFROM T\n",
"Expected CP002 core autofix to normalize identifier capitalisation: {after:?}"
);
}
#[test]
fn test_lint_fix_applies_cp004_core_autofix_in_patch_mode() {
let dir = tempdir().expect("temp dir");
let sql_path = dir.path().join("literal_capitalisation_patch_fix.sql");
std::fs::write(&sql_path, "SELECT NULL, true FROM t\n").expect("write sql");
let output = Command::new(env!("CARGO_BIN_EXE_flowscope"))
.args(["--lint", "--fix", sql_path.to_str().expect("sql path")])
.output()
.expect("run CLI with fix");
assert_ne!(
output.status.code(),
Some(2),
"Expected CLI invocation to succeed: {}",
combined_output(&output)
);
let after = std::fs::read_to_string(&sql_path).expect("read SQL after fix");
assert_eq!(
after, "SELECT\n NULL,\n TRUE\nFROM t\n",
"Expected CP004 core autofix to normalize literal capitalisation: {after:?}"
);
}
#[test]
fn test_lint_fix_applies_cp005_core_autofix_in_patch_mode() {
let dir = tempdir().expect("temp dir");
let sql_path = dir.path().join("type_capitalisation_patch_fix.sql");
std::fs::write(&sql_path, "CREATE TABLE t (a INT, b varchar(10))\n").expect("write sql");
let output = Command::new(env!("CARGO_BIN_EXE_flowscope"))
.args(["--lint", "--fix", sql_path.to_str().expect("sql path")])
.output()
.expect("run CLI with fix");
assert_ne!(
output.status.code(),
Some(2),
"Expected CLI invocation to succeed: {}",
combined_output(&output)
);
let after = std::fs::read_to_string(&sql_path).expect("read SQL after fix");
assert_eq!(
after, "CREATE TABLE t (a INT, b VARCHAR(10))\n",
"Expected CP005 core autofix to normalize type capitalisation: {after:?}"
);
}
#[test]
fn test_lint_fix_applies_cv010_core_autofix_in_patch_mode() {
let dir = tempdir().expect("temp dir");
let sql_path = dir.path().join("quoted_literal_style_patch_fix.sql");
std::fs::write(&sql_path, "SELECT 'abc', \"def\"\n").expect("write sql");
let output = Command::new(env!("CARGO_BIN_EXE_flowscope"))
.args([
"--lint",
"--fix",
"--unsafe-fixes",
"--dialect",
"bigquery",
sql_path.to_str().expect("sql path"),
])
.output()
.expect("run CLI with fix");
assert_ne!(
output.status.code(),
Some(2),
"Expected CLI invocation to succeed: {}",
combined_output(&output)
);
let after = std::fs::read_to_string(&sql_path).expect("read SQL after fix");
assert!(
!after.contains("\"def\""),
"Expected CV010 core autofix to convert double-quoted string to single-quoted: {after:?}"
);
}
#[test]
fn test_lint_fix_applies_rf004_core_autofix_in_patch_mode() {
let dir = tempdir().expect("temp dir");
let sql_path = dir.path().join("references_keywords_patch_fix.sql");
std::fs::write(&sql_path, "select a from users as select\n").expect("write sql");
let output = Command::new(env!("CARGO_BIN_EXE_flowscope"))
.args(["--lint", "--fix", sql_path.to_str().expect("sql path")])
.output()
.expect("run CLI with fix");
assert_ne!(
output.status.code(),
Some(2),
"Expected CLI invocation to succeed: {}",
combined_output(&output)
);
let after = std::fs::read_to_string(&sql_path).expect("read SQL after fix");
assert_eq!(
after, "select a from users\n",
"Expected RF004 core autofix to rewrite keyword table alias safely: {after:?}"
);
}
#[test]
fn test_lint_fix_applies_rf003_core_autofix_in_patch_mode() {
let dir = tempdir().expect("temp dir");
let sql_path = dir.path().join("references_consistent_patch_fix.sql");
std::fs::write(&sql_path, "select a.id, id2 from a\n").expect("write sql");
let output = Command::new(env!("CARGO_BIN_EXE_flowscope"))
.args(["--lint", "--fix", sql_path.to_str().expect("sql path")])
.output()
.expect("run CLI with fix");
assert_ne!(
output.status.code(),
Some(2),
"Expected CLI invocation to succeed: {}",
combined_output(&output)
);
let after = std::fs::read_to_string(&sql_path).expect("read SQL after fix");
assert_eq!(
after, "select\n a.id,\n a.id2\nfrom a\n",
"Expected RF003 core autofix to qualify unqualified references consistently: {after:?}"
);
}
#[test]
fn test_lint_fix_applies_rf006_core_autofix_in_patch_mode() {
let dir = tempdir().expect("temp dir");
let sql_path = dir.path().join("references_quoting_patch_fix.sql");
std::fs::write(&sql_path, "SELECT \"good_name\" FROM t\n").expect("write sql");
let output = Command::new(env!("CARGO_BIN_EXE_flowscope"))
.args(["--lint", "--fix", sql_path.to_str().expect("sql path")])
.output()
.expect("run CLI with fix");
assert_ne!(
output.status.code(),
Some(2),
"Expected CLI invocation to succeed: {}",
combined_output(&output)
);
let after = std::fs::read_to_string(&sql_path).expect("read SQL after fix");
assert_eq!(
after, "SELECT good_name FROM t\n",
"Expected RF006 core autofix to unquote safe identifiers: {after:?}"
);
}
#[test]
fn test_lint_fix_applies_cv003_core_autofix_in_patch_mode() {
let dir = tempdir().expect("temp dir");
let sql_path = dir.path().join("select_trailing_comma_patch_fix.sql");
std::fs::write(&sql_path, "SELECT a, FROM t").expect("write sql");
let output = Command::new(env!("CARGO_BIN_EXE_flowscope"))
.args(["--lint", "--fix", sql_path.to_str().expect("sql path")])
.output()
.expect("run CLI with fix");
assert_ne!(
output.status.code(),
Some(2),
"Expected CLI invocation to succeed: {}",
combined_output(&output)
);
let after = std::fs::read_to_string(&sql_path).expect("read SQL after fix");
assert_eq!(
after, "SELECT a FROM t\n",
"Expected CV003 core autofix to remove trailing comma in patch mode"
);
}
#[test]
fn test_lint_fix_rule_configs_enable_cv003_require_core_autofix_in_patch_mode() {
let dir = tempdir().expect("temp dir");
let sql_path = dir
.path()
.join("select_trailing_comma_require_patch_fix.sql");
std::fs::write(&sql_path, "SELECT a FROM t").expect("write sql");
let output = Command::new(env!("CARGO_BIN_EXE_flowscope"))
.args([
"--lint",
"--fix",
"--rule-configs",
r#"{"convention.select_trailing_comma":{"select_clause_trailing_comma":"require"}}"#,
sql_path.to_str().expect("sql path"),
])
.output()
.expect("run CLI with fix + rule configs");
assert_ne!(
output.status.code(),
Some(2),
"Expected CLI invocation to succeed: {}",
combined_output(&output)
);
let after = std::fs::read_to_string(&sql_path).expect("read SQL after fix");
assert_eq!(
after, "SELECT a, FROM t\n",
"Expected CV003 require-mode core autofix to insert trailing comma before FROM"
);
}
#[test]
fn test_lint_fix_applies_st012_core_autofix_in_patch_mode() {
let dir = tempdir().expect("temp dir");
let sql_path = dir.path().join("consecutive_semicolons_patch_fix.sql");
std::fs::write(&sql_path, "SELECT 1;;").expect("write sql");
let output = Command::new(env!("CARGO_BIN_EXE_flowscope"))
.args(["--lint", "--fix", sql_path.to_str().expect("sql path")])
.output()
.expect("run CLI with fix");
assert_ne!(
output.status.code(),
Some(2),
"Expected CLI invocation to succeed: {}",
combined_output(&output)
);
let after = std::fs::read_to_string(&sql_path).expect("read SQL after fix");
assert_eq!(
after, "SELECT 1;",
"Expected ST012 core autofix to collapse consecutive semicolons"
);
}
#[test]
fn test_lint_fix_applies_lt012_core_autofix_in_patch_mode() {
let dir = tempdir().expect("temp dir");
let sql_path = dir.path().join("single_trailing_newline_patch_fix.sql");
std::fs::write(&sql_path, "SELECT 1\nFROM t").expect("write sql");
let output = Command::new(env!("CARGO_BIN_EXE_flowscope"))
.args(["--lint", "--fix", sql_path.to_str().expect("sql path")])
.output()
.expect("run CLI with fix");
assert_ne!(
output.status.code(),
Some(2),
"Expected CLI invocation to succeed: {}",
combined_output(&output)
);
let after = std::fs::read_to_string(&sql_path).expect("read SQL after fix");
assert_eq!(
after, "SELECT 1\nFROM t\n",
"Expected LT012 core autofix to enforce exactly one trailing newline"
);
}
#[test]
fn test_lint_fix_applies_lt013_core_autofix_in_patch_mode() {
let dir = tempdir().expect("temp dir");
let sql_path = dir.path().join("leading_blank_lines_patch_fix.sql");
std::fs::write(&sql_path, "\n\nSELECT 1").expect("write sql");
let output = Command::new(env!("CARGO_BIN_EXE_flowscope"))
.args(["--lint", "--fix", sql_path.to_str().expect("sql path")])
.output()
.expect("run CLI with fix");
assert_ne!(
output.status.code(),
Some(2),
"Expected CLI invocation to succeed: {}",
combined_output(&output)
);
let after = std::fs::read_to_string(&sql_path).expect("read SQL after fix");
assert!(
after.starts_with("SELECT 1"),
"Expected LT013 core autofix to remove leading blank lines: {after:?}"
);
assert!(
!after.starts_with('\n'),
"Expected LT013 core autofix output to start with SQL text: {after:?}"
);
}
#[test]
fn test_lint_fix_applies_lt014_core_autofix_in_patch_mode() {
let dir = tempdir().expect("temp dir");
let sql_path = dir.path().join("keyword_newline_patch_fix.sql");
std::fs::write(&sql_path, "SELECT a FROM t\nWHERE a = 1").expect("write sql");
let output = Command::new(env!("CARGO_BIN_EXE_flowscope"))
.args(["--lint", "--fix", sql_path.to_str().expect("sql path")])
.output()
.expect("run CLI with fix");
assert_ne!(
output.status.code(),
Some(2),
"Expected CLI invocation to succeed: {}",
combined_output(&output)
);
let after = std::fs::read_to_string(&sql_path).expect("read SQL after fix");
assert!(
after.contains("\nFROM t"),
"Expected LT014 core autofix to line-break major clause keyword: {after:?}"
);
}
#[test]
fn test_lint_fix_applies_lt010_core_autofix_in_patch_mode() {
let dir = tempdir().expect("temp dir");
let sql_path = dir.path().join("select_modifier_patch_fix.sql");
std::fs::write(&sql_path, "SELECT\nDISTINCT a\nFROM t").expect("write sql");
let output = Command::new(env!("CARGO_BIN_EXE_flowscope"))
.args(["--lint", "--fix", sql_path.to_str().expect("sql path")])
.output()
.expect("run CLI with fix");
assert_ne!(
output.status.code(),
Some(2),
"Expected CLI invocation to succeed: {}",
combined_output(&output)
);
let after = std::fs::read_to_string(&sql_path).expect("read SQL after fix");
assert_eq!(
after, "SELECT DISTINCT a\nFROM t\n",
"Expected LT010 core autofix to place DISTINCT on SELECT line: {after:?}"
);
}
#[test]
fn test_lint_fix_applies_lt011_core_autofix_in_patch_mode() {
let dir = tempdir().expect("temp dir");
let sql_path = dir.path().join("set_operator_layout_patch_fix.sql");
std::fs::write(&sql_path, "SELECT 1 UNION SELECT 2\nUNION SELECT 3").expect("write sql");
let output = Command::new(env!("CARGO_BIN_EXE_flowscope"))
.args([
"--lint",
"--fix",
"--exclude-rules",
"LINT_AM_002",
sql_path.to_str().expect("sql path"),
])
.output()
.expect("run CLI with fix");
assert_ne!(
output.status.code(),
Some(2),
"Expected CLI invocation to succeed: {}",
combined_output(&output)
);
let after = std::fs::read_to_string(&sql_path).expect("read SQL after fix");
assert_eq!(
after, "SELECT 1\nUNION\nSELECT 2\nUNION\nSELECT 3\n",
"Expected LT011 core autofix to put set operators on their own line: {after:?}"
);
}
#[test]
fn test_lint_fix_applies_lt007_core_autofix_in_patch_mode() {
let dir = tempdir().expect("temp dir");
let sql_path = dir.path().join("cte_bracket_patch_fix.sql");
std::fs::write(&sql_path, "WITH cte AS (\n SELECT 1)\nSELECT * FROM cte").expect("write sql");
let output = Command::new(env!("CARGO_BIN_EXE_flowscope"))
.args(["--lint", "--fix", sql_path.to_str().expect("sql path")])
.output()
.expect("run CLI with fix");
assert_ne!(
output.status.code(),
Some(2),
"Expected CLI invocation to succeed: {}",
combined_output(&output)
);
let after = std::fs::read_to_string(&sql_path).expect("read SQL after fix");
assert_eq!(
after, "WITH cte AS (\n SELECT 1\n)\n\nSELECT * FROM cte\n",
"Expected LT007 core autofix output in patch mode: {after:?}"
);
}
#[test]
fn test_lint_fix_applies_lt009_core_autofix_in_patch_mode() {
let dir = tempdir().expect("temp dir");
let sql_path = dir.path().join("select_target_layout_patch_fix.sql");
std::fs::write(&sql_path, "select\n a,\n b,\n c from x").expect("write sql");
let output = Command::new(env!("CARGO_BIN_EXE_flowscope"))
.args([
"--lint",
"--fix",
"--exclude-rules",
"LINT_LT_014",
sql_path.to_str().expect("sql path"),
])
.output()
.expect("run CLI with fix");
assert_ne!(
output.status.code(),
Some(2),
"Expected CLI invocation to succeed: {}",
combined_output(&output)
);
let after = std::fs::read_to_string(&sql_path).expect("read SQL after fix");
assert_eq!(
after, "select\n a,\n b,\n c\nfrom x\n",
"Expected LT009 core autofix to place FROM on a new line after final target: {after:?}"
);
}
#[test]
fn test_lint_fix_applies_lt008_core_autofix_in_patch_mode() {
let dir = tempdir().expect("temp dir");
let sql_path = dir.path().join("cte_newline_patch_fix.sql");
std::fs::write(&sql_path, "WITH cte AS (SELECT 1) SELECT * FROM cte").expect("write sql");
let output = Command::new(env!("CARGO_BIN_EXE_flowscope"))
.args(["--lint", "--fix", sql_path.to_str().expect("sql path")])
.output()
.expect("run CLI with fix");
assert_ne!(
output.status.code(),
Some(2),
"Expected CLI invocation to succeed: {}",
combined_output(&output)
);
let after = std::fs::read_to_string(&sql_path).expect("read SQL after fix");
assert_eq!(
after, "WITH cte AS (SELECT 1)\n\nSELECT * FROM cte\n",
"Expected LT008 core autofix to place SELECT on a new line after CTE close: {after:?}"
);
}
#[test]
fn test_lint_fix_applies_lt015_core_autofix_in_patch_mode() {
let dir = tempdir().expect("temp dir");
let sql_path = dir.path().join("layout_newlines_patch_fix.sql");
std::fs::write(&sql_path, "SELECT 1\n\n\nFROM t").expect("write sql");
let output = Command::new(env!("CARGO_BIN_EXE_flowscope"))
.args(["--lint", "--fix", sql_path.to_str().expect("sql path")])
.output()
.expect("run CLI with fix");
assert_ne!(
output.status.code(),
Some(2),
"Expected CLI invocation to succeed: {}",
combined_output(&output)
);
let after = std::fs::read_to_string(&sql_path).expect("read SQL after fix");
assert_eq!(
after, "SELECT 1\n\nFROM t\n",
"Expected LT015 core autofix to collapse excessive blank lines: {after:?}"
);
}
#[test]
fn test_lint_fix_applies_jj001_core_autofix_only_in_unsafe_mode() {
let dir = tempdir().expect("temp dir");
let safe_path = dir.path().join("jinja_padding_safe.sql");
std::fs::write(&safe_path, "SELECT '{{foo}}' AS templated").expect("write sql");
let safe_output = Command::new(env!("CARGO_BIN_EXE_flowscope"))
.args(["--lint", "--fix", safe_path.to_str().expect("sql path")])
.output()
.expect("run CLI with safe fix");
assert_ne!(
safe_output.status.code(),
Some(2),
"Expected CLI invocation to succeed: {}",
combined_output(&safe_output)
);
let safe_after = std::fs::read_to_string(&safe_path).expect("read SQL after safe fix");
assert!(
safe_after.contains("{{foo}}"),
"safe mode should keep template edits protected: {safe_after}"
);
let unsafe_path = dir.path().join("jinja_padding_unsafe.sql");
std::fs::write(&unsafe_path, "SELECT '{{foo}}' AS templated").expect("write sql");
let unsafe_output = Command::new(env!("CARGO_BIN_EXE_flowscope"))
.args([
"--lint",
"--fix",
"--unsafe-fixes",
unsafe_path.to_str().expect("sql path"),
])
.output()
.expect("run CLI with unsafe fix");
assert_ne!(
unsafe_output.status.code(),
Some(2),
"Expected CLI invocation to succeed: {}",
combined_output(&unsafe_output)
);
let unsafe_after = std::fs::read_to_string(&unsafe_path).expect("read SQL after unsafe fix");
assert!(
unsafe_after.contains("{{ foo }}"),
"unsafe mode should apply JJ001 core autofix: {unsafe_after}"
);
}
#[test]
fn test_lint_multiple_files() {
let dir = tempdir().expect("temp dir");
let clean_path = dir.path().join("clean.sql");
let bad_path = dir.path().join("bad.sql");
std::fs::write(&clean_path, SQL_CLEAN).expect("write clean sql");
std::fs::write(&bad_path, SQL_WITH_VIOLATIONS).expect("write bad sql");
let output = Command::new(env!("CARGO_BIN_EXE_flowscope"))
.args([
"--lint",
clean_path.to_str().expect("clean path"),
bad_path.to_str().expect("bad path"),
])
.output()
.expect("run CLI");
let stdout = String::from_utf8_lossy(&output.stdout);
assert_eq!(
output.status.code(),
Some(1),
"Expected exit 1 when any file fails: {stdout}"
);
assert!(
stdout.contains("PASS"),
"Expected PASS for clean file: {stdout}"
);
assert!(
stdout.contains("FAIL"),
"Expected FAIL for bad file: {stdout}"
);
assert!(
stdout.contains("1 file passed"),
"Expected 1 file passed: {stdout}"
);
assert!(
stdout.contains("1 file failed"),
"Expected 1 file failed: {stdout}"
);
assert!(
stdout.contains("All Finished in "),
"Expected elapsed time in summary: {stdout}"
);
}
#[test]
fn test_lint_directory_recursively() {
let dir = tempdir().expect("temp dir");
let nested = dir.path().join("nested");
std::fs::create_dir_all(&nested).expect("create nested directory");
let clean_path = dir.path().join("clean.sql");
let bad_path = nested.join("bad.sql");
let ignored = nested.join("notes.txt");
std::fs::write(&clean_path, SQL_CLEAN).expect("write clean sql");
std::fs::write(&bad_path, SQL_WITH_VIOLATIONS).expect("write bad sql");
std::fs::write(&ignored, SQL_WITH_VIOLATIONS).expect("write ignored file");
let output = Command::new(env!("CARGO_BIN_EXE_flowscope"))
.args(["--lint", dir.path().to_str().expect("dir path")])
.output()
.expect("run CLI");
let stdout = String::from_utf8_lossy(&output.stdout);
assert_eq!(
output.status.code(),
Some(1),
"Expected exit 1 when one discovered SQL file fails: {stdout}"
);
assert!(
stdout.contains("1 file passed"),
"Expected one clean SQL file in recursive lint output: {stdout}"
);
assert!(
stdout.contains("1 file failed"),
"Expected one failing SQL file in recursive lint output: {stdout}"
);
assert!(
stdout.contains("AM02"),
"Expected lint violation from nested SQL file: {stdout}"
);
assert!(
!stdout.contains("notes.txt"),
"Expected non-sql files to be ignored: {stdout}"
);
}
#[test]
fn test_lint_fix_can_modify_commented_sql_while_preserving_comment_bytes() {
let dir = tempdir().expect("temp dir");
let sql_path = dir.path().join("commented_safe_fix.sql");
std::fs::write(&sql_path, SQL_COMMENTED_SAFE_FIX).expect("write sql");
let output = run_flowscope(&["--lint", "--fix", sql_path.to_str().expect("sql path")]);
let after = std::fs::read_to_string(&sql_path).expect("read SQL after fix");
assert_ne!(
after, SQL_COMMENTED_SAFE_FIX,
"Expected --lint --fix to modify commented SQL file"
);
assert!(
after.to_ascii_uppercase().contains("COUNT(*)"),
"Expected safe fix to rewrite COUNT(1): {after}"
);
for comment in ["-- keep:lead", "/* keep:mid */", "-- keep:tail"] {
assert_eq!(
after.matches(comment).count(),
1,
"Expected comment bytes to be preserved exactly once: {comment}; SQL: {after}"
);
}
let output_text = combined_output(&output);
assert!(
!output_text
.to_ascii_lowercase()
.contains("comments are present"),
"Expected comment-aware fixer to avoid whole-file comment skip: {output_text}"
);
}
#[test]
fn test_lint_fix_default_safe_mode_skips_unsafe_or_display_only_candidates() {
let dir = tempdir().expect("temp dir");
let sql_path = dir.path().join("unsafe_candidate.sql");
std::fs::write(&sql_path, SQL_UNSAFE_FIX_REPRESENTATIVE).expect("write sql");
let output = run_flowscope(&["--lint", "--fix", sql_path.to_str().expect("sql path")]);
let output_text = combined_output(&output);
assert_eq!(
output.status.code(),
Some(1),
"Expected remaining violations in safe mode when unsafe/display-only fixes are skipped: {output_text}"
);
let remaining_codes = lint_violation_codes(&sql_path);
assert!(
remaining_codes.iter().any(|code| code == "ST05"),
"Expected representative unsafe violation ST05 to remain in default safe mode: {remaining_codes:?}"
);
assert!(
contains_any(
&output_text,
&[
"unsafe",
"blocked",
"display-only",
"display only",
"safety",
"not applied"
]
),
"Expected safe-mode output to report blocked unsafe/display-only fixes: {output_text}"
);
}
#[test]
fn test_lint_unsafe_fixes_with_legacy_ast_rewrites_enables_additional_fixes_over_safe_mode() {
let dir = tempdir().expect("temp dir");
let safe_path = dir.path().join("safe_mode.sql");
let unsafe_path = dir.path().join("unsafe_mode.sql");
std::fs::write(&safe_path, SQL_UNSAFE_FIX_REPRESENTATIVE).expect("write safe sql");
std::fs::write(&unsafe_path, SQL_UNSAFE_FIX_REPRESENTATIVE).expect("write unsafe sql");
let safe_output = run_flowscope(&["--lint", "--fix", safe_path.to_str().expect("safe path")]);
assert_ne!(
safe_output.status.code(),
Some(2),
"Expected baseline safe fix run to execute, got parse error: {}",
combined_output(&safe_output)
);
let unsafe_output = run_flowscope(&[
"--lint",
"--fix",
"--unsafe-fixes",
"--legacy-ast-fixes",
unsafe_path.to_str().expect("unsafe path"),
]);
assert_flag_was_accepted(&unsafe_output, "--unsafe-fixes");
assert_flag_was_accepted(&unsafe_output, "--legacy-ast-fixes");
let safe_codes = lint_violation_codes(&safe_path);
let unsafe_codes = lint_violation_codes(&unsafe_path);
assert!(
safe_codes.iter().any(|code| code == "ST05"),
"Expected safe mode to retain representative ST05 violation: {safe_codes:?}"
);
assert!(
!unsafe_codes.iter().any(|code| code == "ST05"),
"Expected --unsafe-fixes --legacy-ast-fixes to apply additional structural rewrites and clear ST05. unsafe={unsafe_codes:?}"
);
assert!(
unsafe_codes != safe_codes,
"Expected --unsafe-fixes --legacy-ast-fixes to produce a different lint outcome than safe mode. safe={safe_codes:?}, unsafe={unsafe_codes:?}"
);
}
#[test]
fn test_lint_unsafe_fixes_without_legacy_ast_rewrites_clears_st05_via_core_patch() {
let dir = tempdir().expect("temp dir");
let sql_path = dir.path().join("unsafe_without_legacy.sql");
std::fs::write(&sql_path, SQL_UNSAFE_FIX_REPRESENTATIVE).expect("write sql");
let output = run_flowscope(&[
"--lint",
"--fix",
"--unsafe-fixes",
sql_path.to_str().expect("sql path"),
]);
assert_flag_was_accepted(&output, "--unsafe-fixes");
assert_ne!(
output.status.code(),
Some(2),
"Expected run to execute without clap/parser failure: {}",
combined_output(&output)
);
let codes = lint_violation_codes(&sql_path);
assert!(
!codes.iter().any(|code| code == "ST05"),
"Expected --unsafe-fixes to clear ST05 via core patch rewriter: {codes:?}"
);
}
#[test]
fn test_lint_show_fixes_surfaces_blocked_or_suggested_fix_info() {
let dir = tempdir().expect("temp dir");
let sql_path = dir.path().join("show_fixes.sql");
std::fs::write(&sql_path, SQL_UNSAFE_FIX_REPRESENTATIVE).expect("write baseline sql");
let baseline_output = run_flowscope(&["--lint", "--fix", sql_path.to_str().expect("sql path")]);
assert_ne!(
baseline_output.status.code(),
Some(2),
"Expected baseline run to execute: {}",
combined_output(&baseline_output)
);
std::fs::write(&sql_path, SQL_UNSAFE_FIX_REPRESENTATIVE).expect("reset sql");
let output = run_flowscope(&[
"--lint",
"--fix",
"--show-fixes",
sql_path.to_str().expect("sql path"),
]);
assert_flag_was_accepted(&output, "--show-fixes");
let baseline_stderr = String::from_utf8_lossy(&baseline_output.stderr);
let output_stderr = String::from_utf8_lossy(&output.stderr);
assert_ne!(
output_stderr, baseline_stderr,
"Expected --show-fixes to change stderr by surfacing additional fix visibility details"
);
assert!(
output_stderr.len() > baseline_stderr.len(),
"Expected --show-fixes to provide more stderr detail than baseline --fix output. baseline={baseline_stderr}, show={output_stderr}"
);
assert!(
contains_any(
&output_stderr,
&[
"blocked",
"suggested",
"display-only",
"display only",
"unsafe"
]
),
"Expected --show-fixes output to include blocked/suggested fix details: {output_stderr}"
);
}