1use crate::fix_engine::{
9 apply_edits as apply_patch_edits, derive_protected_ranges, plan_fixes, BlockedReason,
10 Edit as PatchEdit, Fix as PatchFix, FixApplicability as PatchApplicability,
11 ProtectedRange as PatchProtectedRange, ProtectedRangeKind as PatchProtectedRangeKind,
12};
13use flowscope_core::linter::config::canonicalize_rule_code;
14use flowscope_core::{
15 analyze, issue_codes, parse_sql_with_dialect, AnalysisOptions, AnalyzeRequest, Dialect, Issue,
16 IssueAutofixApplicability, LintConfig, ParseError,
17};
18#[cfg(feature = "templating")]
19use flowscope_core::{TemplateConfig, TemplateMode};
20use sqlparser::ast::helpers::attached_token::AttachedToken;
21use sqlparser::ast::*;
22use sqlparser::tokenizer::{Token, TokenWithSpan, Tokenizer, Whitespace};
23use std::borrow::Cow;
24use std::collections::{BTreeMap, HashMap, HashSet, VecDeque};
25use std::hash::{Hash, Hasher};
26use std::time::{Duration, Instant};
27
28fn hash_sql(sql: &str) -> u64 {
30 let mut h = std::collections::hash_map::DefaultHasher::new();
31 sql.hash(&mut h);
32 h.finish()
33}
34
35#[derive(Debug, Default, Clone, PartialEq, Eq)]
36#[must_use]
37pub struct FixCounts {
38 by_rule: BTreeMap<String, usize>,
40}
41
42impl FixCounts {
43 pub fn total(&self) -> usize {
44 self.by_rule.values().sum()
45 }
46
47 pub fn add(&mut self, code: &str, count: usize) {
48 if count == 0 {
49 return;
50 }
51 *self.by_rule.entry(code.to_string()).or_insert(0) += count;
52 }
53
54 pub fn get(&self, code: &str) -> usize {
55 self.by_rule.get(code).copied().unwrap_or(0)
56 }
57
58 pub fn merge(&mut self, other: &Self) {
59 for (code, count) in &other.by_rule {
60 self.add(code, *count);
61 }
62 }
63
64 fn from_removed(before: &BTreeMap<String, usize>, after: &BTreeMap<String, usize>) -> Self {
65 let mut out = Self::default();
66 for (code, before_count) in before {
67 let after_count = after.get(code).copied().unwrap_or(0);
68 if *before_count > after_count {
69 out.add(code, before_count - after_count);
70 }
71 }
72 out
73 }
74}
75
76#[derive(Debug, Clone)]
77#[must_use]
78pub struct FixOutcome {
79 pub sql: String,
80 pub counts: FixCounts,
81 pub changed: bool,
82 pub skipped_due_to_comments: bool,
83 pub skipped_due_to_regression: bool,
84 pub skipped_counts: FixSkippedCounts,
85}
86
87#[derive(Debug, Clone)]
88#[must_use]
89pub struct FixLintState {
90 issues: Vec<Issue>,
91 counts: BTreeMap<String, usize>,
92}
93
94impl FixLintState {
95 fn from_issues(issues: Vec<Issue>) -> Self {
96 let counts = lint_rule_counts_from_issues(&issues);
97 Self { issues, counts }
98 }
99
100 pub fn counts(&self) -> &BTreeMap<String, usize> {
101 &self.counts
102 }
103}
104
105#[derive(Debug, Clone)]
106#[must_use]
107pub struct FixPassResult {
108 pub outcome: FixOutcome,
109 pub post_lint_state: FixLintState,
110}
111
112#[derive(Debug, Default, Clone, PartialEq, Eq)]
113#[must_use]
114pub struct FixSkippedCounts {
115 pub unsafe_skipped: usize,
116 pub protected_range_blocked: usize,
117 pub overlap_conflict_blocked: usize,
118 pub display_only: usize,
119}
120
121#[derive(Debug, Clone, Copy)]
122#[must_use]
123pub struct FixOptions {
124 pub include_unsafe_fixes: bool,
125 pub include_rewrite_candidates: bool,
126}
127
128impl Default for FixOptions {
129 fn default() -> Self {
130 Self {
131 include_unsafe_fixes: false,
132 include_rewrite_candidates: true,
133 }
134 }
135}
136
137const MAX_LINT_FIX_PASSES: usize = 3;
142const MAX_LINT_FIX_BONUS_PASSES: usize = 1;
145const MAX_LINT_FIX_LARGE_SQL_LT02_EXTRA_PASSES: usize = 1;
150const LINT_FIX_LARGE_SQL_LT02_EXTRA_PASS_THRESHOLD: usize = 10_000;
154const LINT_FIX_MOSTLY_UNFIXABLE_STOP_THRESHOLD: usize = 4_000;
157const MAX_RESIDUAL_POTENTIALLY_FIXABLE_FOR_STOP: usize = 2;
160const MOSTLY_UNFIXABLE_RATIO_DENOMINATOR: usize = 5;
163const FIX_LOOP_TIMEOUT: Duration = Duration::from_secs(30);
167
168#[derive(Debug, Clone, Copy, Default)]
172pub struct LintFixRuntimeOptions {
173 pub include_unsafe_fixes: bool,
174 pub legacy_ast_fixes: bool,
175}
176
177#[derive(Debug, Clone, Copy, Default)]
180pub struct FixCandidateStats {
181 pub skipped: usize,
182 pub blocked: usize,
184 pub blocked_unsafe: usize,
185 pub blocked_display_only: usize,
186 pub blocked_protected_range: usize,
187 pub blocked_overlap_conflict: usize,
188}
189
190impl FixCandidateStats {
191 pub fn total_skipped_or_blocked(self) -> usize {
192 self.skipped + self.blocked
193 }
194
195 pub fn merge(&mut self, other: Self) {
196 self.skipped += other.skipped;
197 self.blocked += other.blocked;
198 self.blocked_unsafe += other.blocked_unsafe;
199 self.blocked_display_only += other.blocked_display_only;
200 self.blocked_protected_range += other.blocked_protected_range;
201 self.blocked_overlap_conflict += other.blocked_overlap_conflict;
202 }
203}
204
205#[derive(Debug, Clone)]
210pub struct LintFixExecution {
211 pub outcome: FixOutcome,
212 pub candidate_stats: FixCandidateStats,
213}
214
215#[derive(Debug, Clone, Default)]
216struct RuleFilter {
217 disabled: HashSet<String>,
218 st005_forbid_subquery_in: St005ForbidSubqueryIn,
219}
220
221#[derive(Debug, Clone, Copy, Eq, PartialEq, Default)]
222enum St005ForbidSubqueryIn {
223 Both,
224 #[default]
225 Join,
226 From,
227}
228
229impl St005ForbidSubqueryIn {
230 fn forbid_from(self) -> bool {
231 matches!(self, Self::Both | Self::From)
232 }
233
234 fn forbid_join(self) -> bool {
235 matches!(self, Self::Both | Self::Join)
236 }
237}
238
239impl RuleFilter {
240 fn from_lint_config(lint_config: &LintConfig) -> Self {
241 let disabled: HashSet<String> = lint_config
242 .disabled_rules
243 .iter()
244 .filter_map(|rule| {
245 let trimmed = rule.trim();
246 if trimmed.is_empty() {
247 return None;
248 }
249 Some(
250 canonicalize_rule_code(trimmed).unwrap_or_else(|| trimmed.to_ascii_uppercase()),
251 )
252 })
253 .collect();
254 let st005_forbid_subquery_in = match lint_config
255 .rule_option_str(issue_codes::LINT_ST_005, "forbid_subquery_in")
256 .unwrap_or("join")
257 .to_ascii_lowercase()
258 .as_str()
259 {
260 "from" => St005ForbidSubqueryIn::From,
261 "both" => St005ForbidSubqueryIn::Both,
262 _ => St005ForbidSubqueryIn::Join,
263 };
264 Self {
265 disabled,
266 st005_forbid_subquery_in,
267 }
268 }
269
270 fn allows(&self, code: &str) -> bool {
271 let canonical =
272 canonicalize_rule_code(code).unwrap_or_else(|| code.trim().to_ascii_uppercase());
273 !self.disabled.contains(&canonical)
274 }
275
276 fn with_rule_disabled(&self, code: &str) -> Self {
277 let mut updated = self.clone();
278 let canonical =
279 canonicalize_rule_code(code).unwrap_or_else(|| code.trim().to_ascii_uppercase());
280 updated.disabled.insert(canonical);
281 updated
282 }
283}
284
285struct FixProfileGuard {
286 enabled: bool,
287 started_at: Instant,
288 sql_len: usize,
289 include_rewrite_candidates: bool,
290 include_unsafe_fixes: bool,
291 marks: Vec<(&'static str, Duration)>,
292}
293
294impl FixProfileGuard {
295 fn new(sql_len: usize, fix_options: FixOptions) -> Self {
296 Self {
297 enabled: std::env::var_os("FLOWSCOPE_FIX_PROFILE").is_some(),
298 started_at: Instant::now(),
299 sql_len,
300 include_rewrite_candidates: fix_options.include_rewrite_candidates,
301 include_unsafe_fixes: fix_options.include_unsafe_fixes,
302 marks: Vec::new(),
303 }
304 }
305
306 fn record(&mut self, label: &'static str, started: Instant) {
307 if self.enabled {
308 self.marks.push((label, started.elapsed()));
309 }
310 }
311}
312
313impl Drop for FixProfileGuard {
314 fn drop(&mut self) {
315 if !self.enabled {
316 return;
317 }
318
319 let total = self.started_at.elapsed();
320 let mut summary = format!(
321 "flowscope: fix-profile sql_len={} rewrite={} unsafe={} total={:.2}ms",
322 self.sql_len,
323 self.include_rewrite_candidates,
324 self.include_unsafe_fixes,
325 total.as_secs_f64() * 1000.0
326 );
327 for (label, duration) in &self.marks {
328 summary.push_str(&format!(
329 " | {label}={:.2}ms",
330 duration.as_secs_f64() * 1000.0
331 ));
332 }
333 eprintln!("{summary}");
334 }
335}
336
337pub fn apply_lint_fixes(
343 sql: &str,
344 dialect: Dialect,
345 disabled_rules: &[String],
346) -> Result<FixOutcome, ParseError> {
347 apply_lint_fixes_with_lint_config(
348 sql,
349 dialect,
350 &LintConfig {
351 enabled: true,
352 disabled_rules: disabled_rules.to_vec(),
353 rule_configs: BTreeMap::new(),
354 },
355 )
356}
357
358pub fn apply_lint_fixes_with_lint_config(
359 sql: &str,
360 dialect: Dialect,
361 lint_config: &LintConfig,
362) -> Result<FixOutcome, ParseError> {
363 apply_lint_fixes_with_options(
364 sql,
365 dialect,
366 lint_config,
367 FixOptions {
368 include_unsafe_fixes: true,
370 include_rewrite_candidates: true,
371 },
372 )
373}
374
375pub fn apply_lint_fixes_with_options_and_lint_state(
376 sql: &str,
377 dialect: Dialect,
378 lint_config: &LintConfig,
379 fix_options: FixOptions,
380 before_lint_state: Option<FixLintState>,
381) -> Result<FixPassResult, ParseError> {
382 apply_lint_fixes_with_options_internal(
383 sql,
384 dialect,
385 lint_config,
386 fix_options,
387 before_lint_state,
388 )
389}
390
391pub fn apply_lint_fixes_with_options(
392 sql: &str,
393 dialect: Dialect,
394 lint_config: &LintConfig,
395 fix_options: FixOptions,
396) -> Result<FixOutcome, ParseError> {
397 Ok(
398 apply_lint_fixes_with_options_internal(sql, dialect, lint_config, fix_options, None)?
399 .outcome,
400 )
401}
402
403pub fn apply_lint_fixes_with_runtime_options(
409 sql: &str,
410 dialect: Dialect,
411 lint_config: &LintConfig,
412 runtime_options: LintFixRuntimeOptions,
413) -> Result<LintFixExecution, ParseError> {
414 let fix_options = FixOptions {
415 include_unsafe_fixes: runtime_options.include_unsafe_fixes,
416 include_rewrite_candidates: runtime_options.legacy_ast_fixes,
417 };
418
419 let mut current_sql = sql.to_string();
420 let mut merged_counts = FixCounts::default();
421 let mut merged_candidate_stats = FixCandidateStats::default();
422 let mut any_changed = false;
423 let mut lt03_touched = false;
424 let mut lt02_touched = false;
425 let mut last_outcome = None;
426 let mut cached_lint_state: Option<FixLintState> = None;
427 let mut seen_sql: HashSet<u64> = HashSet::from([hash_sql(¤t_sql)]);
428 let mut overlap_retried_sql: HashSet<u64> = HashSet::new();
429 let mut pass_limit = MAX_LINT_FIX_PASSES;
430 let mut bonus_passes_granted = 0usize;
431 let mut large_sql_lt02_extra_passes_granted = 0usize;
432 let mut pass_index = 0usize;
433 let fix_started_at = Instant::now();
434
435 while pass_index < pass_limit {
436 if pass_index > 0 && fix_started_at.elapsed() >= FIX_LOOP_TIMEOUT {
437 break;
438 }
439 let pass_result = apply_lint_fixes_with_options_and_lint_state(
440 ¤t_sql,
441 dialect,
442 lint_config,
443 fix_options,
444 cached_lint_state.take(),
445 )?;
446 let outcome = pass_result.outcome;
447 let post_lint_state = pass_result.post_lint_state;
448
449 if outcome.changed && !seen_sql.insert(hash_sql(&outcome.sql)) {
451 break;
452 }
453
454 merged_counts.merge(&outcome.counts);
455 merged_candidate_stats.merge(collect_fix_candidate_stats(&outcome, runtime_options));
456 if outcome.counts.get(issue_codes::LINT_LT_003) > 0 {
457 lt03_touched = true;
458 }
459 if outcome.counts.get(issue_codes::LINT_LT_002) > 0 {
460 lt02_touched = true;
461 }
462 let lt_cleanup_progress = outcome.counts.get(issue_codes::LINT_LT_003) > 0
463 || outcome.counts.get(issue_codes::LINT_LT_002) > 0;
464 let lt02_remaining = post_lint_state
465 .counts()
466 .get(issue_codes::LINT_LT_002)
467 .copied()
468 .unwrap_or(0)
469 > 0;
470 let residual_is_mostly_unfixable = is_mostly_unfixable_residual(post_lint_state.counts());
471
472 if outcome.changed {
473 any_changed = true;
474 current_sql = outcome.sql.clone();
475 }
476 cached_lint_state = Some(post_lint_state);
477
478 let mut continue_fixing = outcome.changed
479 && !outcome.skipped_due_to_comments
480 && !outcome.skipped_due_to_regression;
481 if continue_fixing
482 && should_stop_large_mostly_unfixable(
483 pass_index,
484 current_sql.len(),
485 lt02_touched,
486 lt03_touched,
487 lt_cleanup_progress,
488 residual_is_mostly_unfixable,
489 )
490 {
491 continue_fixing = false;
492 }
493 let overlap_retry = !outcome.changed
496 && !outcome.skipped_due_to_comments
497 && !outcome.skipped_due_to_regression
498 && outcome.skipped_counts.overlap_conflict_blocked > 0
499 && overlap_retried_sql.insert(hash_sql(¤t_sql));
500
501 if (continue_fixing || overlap_retry)
504 && pass_index + 1 == pass_limit
505 && bonus_passes_granted < MAX_LINT_FIX_BONUS_PASSES
506 && (overlap_retry || lt03_touched || lt02_touched)
507 {
508 pass_limit += 1;
509 bonus_passes_granted += 1;
510 }
511
512 if continue_fixing
513 && pass_index + 1 == pass_limit
514 && bonus_passes_granted >= MAX_LINT_FIX_BONUS_PASSES
515 && large_sql_lt02_extra_passes_granted < MAX_LINT_FIX_LARGE_SQL_LT02_EXTRA_PASSES
516 && current_sql.len() >= LINT_FIX_LARGE_SQL_LT02_EXTRA_PASS_THRESHOLD
517 && lt02_remaining
518 {
519 pass_limit += 1;
520 large_sql_lt02_extra_passes_granted += 1;
521 }
522
523 last_outcome = Some(outcome);
524
525 if !continue_fixing && !overlap_retry {
526 break;
527 }
528
529 pass_index += 1;
530 }
531
532 let mut outcome = last_outcome.expect("at least one fix pass should run");
533 if any_changed {
534 outcome.sql = current_sql;
535 outcome.changed = true;
536 outcome.counts = merged_counts;
537 outcome.skipped_due_to_comments = false;
539 outcome.skipped_due_to_regression = false;
540 }
541
542 Ok(LintFixExecution {
543 outcome,
544 candidate_stats: merged_candidate_stats,
545 })
546}
547
548fn collect_fix_candidate_stats(
549 outcome: &FixOutcome,
550 runtime_options: LintFixRuntimeOptions,
551) -> FixCandidateStats {
552 let blocked_unsafe = if runtime_options.include_unsafe_fixes {
553 0
554 } else {
555 outcome.skipped_counts.unsafe_skipped
556 };
557 let blocked_display_only = outcome.skipped_counts.display_only;
558 let blocked_protected_range = outcome.skipped_counts.protected_range_blocked;
559 let blocked_overlap_conflict = outcome.skipped_counts.overlap_conflict_blocked;
560 let blocked =
561 blocked_unsafe + blocked_display_only + blocked_protected_range + blocked_overlap_conflict;
562
563 let stats = FixCandidateStats {
564 skipped: 0,
565 blocked,
566 blocked_unsafe,
567 blocked_display_only,
568 blocked_protected_range,
569 blocked_overlap_conflict,
570 };
571 debug_assert_eq!(
572 stats.blocked,
573 stats.blocked_unsafe
574 + stats.blocked_display_only
575 + stats.blocked_protected_range
576 + stats.blocked_overlap_conflict,
577 "blocked total must equal sum of blocked_* components"
578 );
579 stats
580}
581
582fn is_mostly_unfixable_rule(code: &str) -> bool {
583 matches!(
584 code,
585 issue_codes::LINT_AL_003
586 | issue_codes::LINT_RF_002
587 | issue_codes::LINT_RF_004
588 | issue_codes::LINT_LT_005
589 )
590}
591
592fn is_mostly_unfixable_residual(after_counts: &BTreeMap<String, usize>) -> bool {
593 let mut residual_total = 0usize;
594 let mut potentially_fixable = 0usize;
595
596 for (code, count) in after_counts {
597 if *count == 0 || code == issue_codes::PARSE_ERROR {
598 continue;
599 }
600 residual_total += *count;
601 if !is_mostly_unfixable_rule(code) {
602 potentially_fixable += *count;
603 if potentially_fixable > MAX_RESIDUAL_POTENTIALLY_FIXABLE_FOR_STOP {
604 return false;
605 }
606 }
607 }
608
609 if residual_total == 0 {
610 return false;
611 }
612
613 potentially_fixable * MOSTLY_UNFIXABLE_RATIO_DENOMINATOR <= residual_total
614}
615
616fn should_stop_large_mostly_unfixable(
620 pass_index: usize,
621 sql_len: usize,
622 lt02_touched: bool,
623 lt03_touched: bool,
624 lt_cleanup_progress: bool,
625 residual_is_mostly_unfixable: bool,
626) -> bool {
627 pass_index + 1 >= MAX_LINT_FIX_PASSES
628 && sql_len >= LINT_FIX_MOSTLY_UNFIXABLE_STOP_THRESHOLD
629 && !lt02_touched
630 && !lt03_touched
631 && !lt_cleanup_progress
632 && residual_is_mostly_unfixable
633}
634
635fn apply_lint_fixes_with_options_internal(
636 sql: &str,
637 dialect: Dialect,
638 lint_config: &LintConfig,
639 fix_options: FixOptions,
640 before_lint_state: Option<FixLintState>,
641) -> Result<FixPassResult, ParseError> {
642 let mut profile = FixProfileGuard::new(sql.len(), fix_options);
643 const INCREMENTAL_LARGE_SQL_THRESHOLD: usize = 4_000;
647
648 const INCREMENTAL_MAX_ITERATIONS_PARSE_ERROR: usize = 4;
652 const INCREMENTAL_MAX_ITERATIONS_PARSE_ERROR_LARGE_SQL: usize = 1;
653 const INCREMENTAL_MAX_RULE_EVALUATIONS_PARSE_ERROR_LARGE_SQL: usize = 1;
654
655 const INCREMENTAL_MAX_ITERATIONS_DEFAULT: usize = 24;
658 const INCREMENTAL_MAX_ITERATIONS_DEFAULT_LARGE_SQL: usize = 12;
659
660 const INCREMENTAL_MAX_ITERATIONS_OVERLAP_RECOVERY: usize = 8;
665 const INCREMENTAL_MAX_ITERATIONS_OVERLAP_RECOVERY_LARGE_SQL: usize = 1;
666 const INCREMENTAL_MAX_RULE_EVALUATIONS_OVERLAP_RECOVERY_LARGE_SQL: usize = 1;
667 let is_large_sql = sql.len() >= INCREMENTAL_LARGE_SQL_THRESHOLD;
668 let incremental_parse_error_iterations = if is_large_sql {
669 INCREMENTAL_MAX_ITERATIONS_PARSE_ERROR_LARGE_SQL
670 } else {
671 INCREMENTAL_MAX_ITERATIONS_PARSE_ERROR
672 };
673 let incremental_default_iterations = if is_large_sql {
674 INCREMENTAL_MAX_ITERATIONS_DEFAULT_LARGE_SQL
675 } else {
676 INCREMENTAL_MAX_ITERATIONS_DEFAULT
677 };
678 let incremental_parse_error_rule_evaluations = if is_large_sql {
679 INCREMENTAL_MAX_RULE_EVALUATIONS_PARSE_ERROR_LARGE_SQL
680 } else {
681 usize::MAX
682 };
683 let incremental_overlap_recovery_iterations = if is_large_sql {
684 INCREMENTAL_MAX_ITERATIONS_OVERLAP_RECOVERY_LARGE_SQL
685 } else {
686 INCREMENTAL_MAX_ITERATIONS_OVERLAP_RECOVERY
687 };
688 let incremental_overlap_recovery_rule_evaluations = if is_large_sql {
689 INCREMENTAL_MAX_RULE_EVALUATIONS_OVERLAP_RECOVERY_LARGE_SQL
690 } else {
691 usize::MAX
692 };
693 let rule_filter = RuleFilter::from_lint_config(lint_config);
694
695 let (before_issues, before_counts) = if let Some(state) = before_lint_state {
696 let stage_started = Instant::now();
697 profile.record("lint_state_cached", stage_started);
698 (state.issues, state.counts)
699 } else {
700 let stage_started = Instant::now();
701 let before_issues = lint_issues(sql, dialect, lint_config);
702 profile.record("lint_issues_before", stage_started);
703
704 let stage_started = Instant::now();
705 let before_counts = lint_rule_counts_from_issues(&before_issues);
706 profile.record("before_counts", stage_started);
707 (before_issues, before_counts)
708 };
709
710 let stage_started = Instant::now();
711 let mut core_candidates = build_fix_candidates_from_issue_autofixes(sql, &before_issues);
712 core_candidates.extend(build_al001_fallback_candidates(
713 sql,
714 dialect,
715 &before_issues,
716 lint_config,
717 ));
718 profile.record("core_candidates", stage_started);
719
720 let stage_started = Instant::now();
721 let core_autofix_rules =
722 collect_core_autofix_rules(&before_issues, fix_options.include_unsafe_fixes);
723 profile.record("core_autofix_rules", stage_started);
724 let mut candidates = Vec::new();
725
726 if fix_options.include_rewrite_candidates {
727 let rewrite_stage_started = Instant::now();
728 let safe_rule_filter = if fix_options.include_unsafe_fixes {
729 rule_filter.clone()
730 } else {
731 rule_filter.with_rule_disabled(issue_codes::LINT_ST_005)
734 };
735
736 let mut statements = parse_sql_with_dialect(sql, dialect)?;
737 for stmt in &mut statements {
738 fix_statement(stmt, &safe_rule_filter);
739 }
740
741 let rewritten_sql = render_statements(&statements, sql);
742 let rewritten_sql = if safe_rule_filter.allows(issue_codes::LINT_AL_001) {
743 apply_configured_table_alias_style(&rewritten_sql, dialect, lint_config)
744 } else {
745 preserve_original_table_alias_style(sql, &rewritten_sql, dialect)
746 };
747
748 let mut rewrite_candidates = build_fix_candidates_from_rewrite(
749 sql,
750 &rewritten_sql,
751 FixCandidateApplicability::Safe,
752 FixCandidateSource::PrimaryRewrite,
753 );
754 if !fix_options.include_unsafe_fixes {
755 let mut unsafe_statements = parse_sql_with_dialect(sql, dialect)?;
756 for stmt in &mut unsafe_statements {
757 fix_statement(stmt, &rule_filter);
758 }
759 let unsafe_sql = render_statements(&unsafe_statements, sql);
760 let unsafe_sql = if rule_filter.allows(issue_codes::LINT_AL_001) {
761 apply_configured_table_alias_style(&unsafe_sql, dialect, lint_config)
762 } else {
763 preserve_original_table_alias_style(sql, &unsafe_sql, dialect)
764 };
765 if unsafe_sql != rewritten_sql {
766 rewrite_candidates.extend(build_fix_candidates_from_rewrite(
767 sql,
768 &unsafe_sql,
769 FixCandidateApplicability::Unsafe,
770 FixCandidateSource::UnsafeFallback,
771 ));
772 }
773 }
774
775 candidates.extend(rewrite_candidates);
776 profile.record("rewrite_candidates", rewrite_stage_started);
777 }
778
779 candidates.extend(core_candidates.iter().cloned());
780
781 let stage_started = Instant::now();
782 let protected_ranges =
783 collect_comment_protected_ranges(sql, dialect, !fix_options.include_unsafe_fixes);
784 profile.record("protected_ranges", stage_started);
785
786 let stage_started = Instant::now();
787 let planned = plan_fix_candidates(
788 sql,
789 candidates,
790 &protected_ranges,
791 fix_options.include_unsafe_fixes,
792 );
793 profile.record("plan_fix_candidates", stage_started);
794
795 let stage_started = Instant::now();
796 let mut fixed_sql = apply_planned_edits(sql, &planned.edits);
797 profile.record("apply_planned_edits", stage_started);
798
799 let stage_started = Instant::now();
800 let mut after_lint_state = if fixed_sql == sql {
801 FixLintState {
802 issues: before_issues.clone(),
803 counts: before_counts.clone(),
804 }
805 } else {
806 lint_state(&fixed_sql, dialect, lint_config)
807 };
808 let mut after_counts = after_lint_state.counts.clone();
809 profile.record("after_counts", stage_started);
810
811 let before_total = regression_guard_total(&before_counts);
812 let after_total = regression_guard_total(&after_counts);
813 let mut skipped_counts = planned.skipped.clone();
814
815 if parse_errors_increased(&before_counts, &after_counts) {
816 if let Some(result) = try_fallback_fix(
817 sql,
818 dialect,
819 lint_config,
820 &before_counts,
821 &before_issues,
822 &core_candidates,
823 &protected_ranges,
824 fix_options.include_unsafe_fixes,
825 incremental_parse_error_iterations,
826 incremental_parse_error_rule_evaluations,
827 &mut profile,
828 "fallback_core_only_parse_errors",
829 "fallback_incremental_parse_errors",
830 ) {
831 return Ok(result);
832 }
833
834 return Ok(FixPassResult {
835 post_lint_state: FixLintState {
836 issues: before_issues.clone(),
837 counts: before_counts.clone(),
838 },
839 outcome: FixOutcome {
840 sql: sql.to_string(),
841 counts: FixCounts::default(),
842 changed: false,
843 skipped_due_to_comments: false,
844 skipped_due_to_regression: true,
845 skipped_counts,
846 },
847 });
848 }
849
850 if fix_options.include_rewrite_candidates
851 && core_autofix_rules_not_improved(&before_counts, &after_counts, &core_autofix_rules)
852 {
853 if let Some(result) = try_fallback_fix(
854 sql,
855 dialect,
856 lint_config,
857 &before_counts,
858 &before_issues,
859 &core_candidates,
860 &protected_ranges,
861 fix_options.include_unsafe_fixes,
862 incremental_default_iterations,
863 usize::MAX,
864 &mut profile,
865 "fallback_core_only_rewrite_guard",
866 "fallback_incremental_rewrite_guard",
867 ) {
868 return Ok(result);
869 }
870 }
871
872 let masked_or_worse = after_total > before_total
876 || (after_total == before_total
877 && after_counts != before_counts
878 && core_autofix_rules_not_improved(&before_counts, &after_counts, &core_autofix_rules));
879 if masked_or_worse {
880 if let Some(result) = try_fallback_fix(
881 sql,
882 dialect,
883 lint_config,
884 &before_counts,
885 &before_issues,
886 &core_candidates,
887 &protected_ranges,
888 fix_options.include_unsafe_fixes,
889 incremental_default_iterations,
890 usize::MAX,
891 &mut profile,
892 "fallback_core_only_masked_or_worse",
893 "fallback_incremental_masked_or_worse",
894 ) {
895 return Ok(result);
896 }
897
898 return Ok(FixPassResult {
899 post_lint_state: FixLintState {
900 issues: before_issues.clone(),
901 counts: before_counts.clone(),
902 },
903 outcome: FixOutcome {
904 sql: sql.to_string(),
905 counts: FixCounts::default(),
906 changed: false,
907 skipped_due_to_comments: false,
908 skipped_due_to_regression: true,
909 skipped_counts,
910 },
911 });
912 }
913
914 const MAX_OVERLAP_CONFLICTS_FOR_INCREMENTAL_RECOVERY: usize = 8;
918 const MAX_OVERLAP_CONFLICTS_FOR_INCREMENTAL_RECOVERY_LARGE_SQL: usize = 8;
919 let overlap_recovery_conflict_limit = if is_large_sql {
920 MAX_OVERLAP_CONFLICTS_FOR_INCREMENTAL_RECOVERY_LARGE_SQL
921 } else {
922 MAX_OVERLAP_CONFLICTS_FOR_INCREMENTAL_RECOVERY
923 };
924 if !fix_options.include_rewrite_candidates
925 && skipped_counts.overlap_conflict_blocked > 0
926 && skipped_counts.overlap_conflict_blocked <= overlap_recovery_conflict_limit
927 {
928 let stage_started = Instant::now();
929 if let Some(incremental) = try_incremental_core_fix_plan(
930 &fixed_sql,
931 dialect,
932 lint_config,
933 &after_counts,
934 Some(after_lint_state.issues.as_slice()),
935 fix_options.include_unsafe_fixes,
936 incremental_overlap_recovery_iterations,
937 incremental_overlap_recovery_rule_evaluations,
938 ) {
939 profile.record("incremental_overlap_recovery", stage_started);
940 merge_skipped_counts(&mut skipped_counts, &incremental.skipped_counts);
941 fixed_sql = incremental.sql;
942 let recount_started = Instant::now();
943 after_lint_state = lint_state(&fixed_sql, dialect, lint_config);
944 after_counts = after_lint_state.counts.clone();
945 profile.record("after_counts_overlap_recovery", recount_started);
946 } else {
947 profile.record("incremental_overlap_recovery", stage_started);
948 }
949 }
950
951 let stage_started = Instant::now();
952 let counts = FixCounts::from_removed(&before_counts, &after_counts);
953 profile.record("final_removed_counts", stage_started);
954
955 if counts.total() == 0 {
956 return Ok(FixPassResult {
957 post_lint_state: FixLintState {
958 issues: before_issues.clone(),
959 counts: before_counts.clone(),
960 },
961 outcome: FixOutcome {
962 sql: sql.to_string(),
963 counts,
964 changed: false,
965 skipped_due_to_comments: false,
966 skipped_due_to_regression: false,
967 skipped_counts,
968 },
969 });
970 }
971 let changed = fixed_sql != sql;
972
973 Ok(FixPassResult {
974 post_lint_state: after_lint_state,
975 outcome: FixOutcome {
976 sql: fixed_sql,
977 counts,
978 changed,
979 skipped_due_to_comments: false,
980 skipped_due_to_regression: false,
981 skipped_counts,
982 },
983 })
984}
985
986#[cfg(test)]
988fn contains_comment_markers(sql: &str, dialect: Dialect) -> bool {
989 #[derive(Clone, Copy, PartialEq, Eq)]
990 enum ScanMode {
991 Outside,
992 SingleQuote,
993 DoubleQuote,
994 BacktickQuote,
995 BracketQuote,
996 }
997
998 let bytes = sql.as_bytes();
999 let mut mode = ScanMode::Outside;
1000 let mut i = 0usize;
1001
1002 while i < bytes.len() {
1003 let b = bytes[i];
1004 let next = bytes.get(i + 1).copied();
1005
1006 match mode {
1007 ScanMode::Outside => {
1008 if b == b'\'' {
1009 mode = ScanMode::SingleQuote;
1010 i += 1;
1011 continue;
1012 }
1013 if b == b'"' {
1014 mode = ScanMode::DoubleQuote;
1015 i += 1;
1016 continue;
1017 }
1018 if b == b'`' {
1019 mode = ScanMode::BacktickQuote;
1020 i += 1;
1021 continue;
1022 }
1023 if b == b'[' {
1024 mode = ScanMode::BracketQuote;
1025 i += 1;
1026 continue;
1027 }
1028
1029 if b == b'-' && next == Some(b'-') {
1030 return true;
1031 }
1032 if b == b'/' && next == Some(b'*') {
1033 return true;
1034 }
1035 if matches!(dialect, Dialect::Mysql) && b == b'#' {
1036 return true;
1037 }
1038
1039 i += 1;
1040 }
1041 ScanMode::SingleQuote => {
1042 if b == b'\'' && next == Some(b'\'') {
1043 i += 2;
1044 } else if b == b'\'' {
1045 mode = ScanMode::Outside;
1046 i += 1;
1047 } else {
1048 i += 1;
1049 }
1050 }
1051 ScanMode::DoubleQuote => {
1052 if b == b'"' && next == Some(b'"') {
1053 i += 2;
1054 } else if b == b'"' {
1055 mode = ScanMode::Outside;
1056 i += 1;
1057 } else {
1058 i += 1;
1059 }
1060 }
1061 ScanMode::BacktickQuote => {
1062 if b == b'`' && next == Some(b'`') {
1063 i += 2;
1064 } else if b == b'`' {
1065 mode = ScanMode::Outside;
1066 i += 1;
1067 } else {
1068 i += 1;
1069 }
1070 }
1071 ScanMode::BracketQuote => {
1072 if b == b']' && next == Some(b']') {
1073 i += 2;
1074 } else if b == b']' {
1075 mode = ScanMode::Outside;
1076 i += 1;
1077 } else {
1078 i += 1;
1079 }
1080 }
1081 }
1082 }
1083
1084 false
1085}
1086
1087fn render_statements(statements: &[Statement], original: &str) -> String {
1088 let mut rendered = statements
1089 .iter()
1090 .map(ToString::to_string)
1091 .collect::<Vec<_>>()
1092 .join(";\n");
1093
1094 if statements.len() > 1 || original.trim_end().ends_with(';') {
1095 rendered.push(';');
1096 }
1097
1098 rendered
1099}
1100
1101fn lint_rule_counts(
1102 sql: &str,
1103 dialect: Dialect,
1104 lint_config: &LintConfig,
1105) -> BTreeMap<String, usize> {
1106 let issues = lint_issues(sql, dialect, lint_config);
1107 lint_rule_counts_from_issues(&issues)
1108}
1109
1110fn lint_state(sql: &str, dialect: Dialect, lint_config: &LintConfig) -> FixLintState {
1111 let issues = lint_issues(sql, dialect, lint_config);
1112 FixLintState::from_issues(issues)
1113}
1114
1115fn lint_issues(sql: &str, dialect: Dialect, lint_config: &LintConfig) -> Vec<Issue> {
1116 let mut result = analyze(&AnalyzeRequest {
1117 sql: sql.to_string(),
1118 files: None,
1119 dialect,
1120 source_name: None,
1121 options: Some(AnalysisOptions {
1122 lint: Some(lint_config.clone()),
1123 ..Default::default()
1124 }),
1125 schema: None,
1126 #[cfg(feature = "templating")]
1127 template_config: None,
1128 });
1129
1130 #[cfg(feature = "templating")]
1131 {
1132 if contains_template_markers(sql)
1133 && issues_have_parse_errors(&result.issues)
1134 && template_retry_enabled_for_fixes(lint_config)
1135 {
1136 let jinja_result = analyze(&AnalyzeRequest {
1137 sql: sql.to_string(),
1138 files: None,
1139 dialect,
1140 source_name: None,
1141 options: Some(AnalysisOptions {
1142 lint: Some(lint_config.clone()),
1143 ..Default::default()
1144 }),
1145 schema: None,
1146 template_config: Some(TemplateConfig {
1147 mode: TemplateMode::Jinja,
1148 context: HashMap::new(),
1149 }),
1150 });
1151
1152 result = if issues_have_template_errors(&jinja_result.issues) {
1153 analyze(&AnalyzeRequest {
1154 sql: sql.to_string(),
1155 files: None,
1156 dialect,
1157 source_name: None,
1158 options: Some(AnalysisOptions {
1159 lint: Some(lint_config.clone()),
1160 ..Default::default()
1161 }),
1162 schema: None,
1163 template_config: Some(TemplateConfig {
1164 mode: TemplateMode::Dbt,
1165 context: HashMap::new(),
1166 }),
1167 })
1168 } else {
1169 jinja_result
1170 };
1171 }
1172 }
1173
1174 result
1175 .issues
1176 .into_iter()
1177 .filter(|issue| issue.code.starts_with("LINT_") || issue.code == issue_codes::PARSE_ERROR)
1178 .collect()
1179}
1180
1181#[cfg(feature = "templating")]
1182fn contains_template_markers(sql: &str) -> bool {
1183 sql.contains("{{") || sql.contains("{%") || sql.contains("{#")
1184}
1185
1186#[cfg(feature = "templating")]
1187fn template_retry_enabled_for_fixes(lint_config: &LintConfig) -> bool {
1188 let registry_config = LintConfig {
1189 enabled: true,
1190 disabled_rules: vec![],
1191 rule_configs: BTreeMap::new(),
1192 };
1193 let enabled_codes: Vec<String> = flowscope_core::linter::rules::all_rules(®istry_config)
1194 .into_iter()
1195 .map(|rule| rule.code().to_string())
1196 .filter(|code| lint_config.is_rule_enabled(code))
1197 .collect();
1198
1199 if enabled_codes.len() != 1 {
1200 return false;
1201 }
1202
1203 let only_code = &enabled_codes[0];
1204 only_code.eq_ignore_ascii_case(issue_codes::LINT_LT_004)
1205 || only_code.eq_ignore_ascii_case(issue_codes::LINT_LT_007)
1206 || only_code.eq_ignore_ascii_case(issue_codes::LINT_CP_003)
1207}
1208
1209#[cfg(feature = "templating")]
1210fn issues_have_parse_errors(issues: &[Issue]) -> bool {
1211 issues
1212 .iter()
1213 .any(|issue| issue.code == issue_codes::PARSE_ERROR)
1214}
1215
1216#[cfg(feature = "templating")]
1217fn issues_have_template_errors(issues: &[Issue]) -> bool {
1218 issues
1219 .iter()
1220 .any(|issue| issue.code == issue_codes::TEMPLATE_ERROR)
1221}
1222
1223fn lint_rule_counts_from_issues(issues: &[Issue]) -> BTreeMap<String, usize> {
1224 let mut counts = BTreeMap::new();
1225 for issue in issues {
1226 *counts.entry(issue.code.clone()).or_insert(0usize) += 1;
1227 }
1228 counts
1229}
1230
1231fn collect_core_autofix_rules(issues: &[Issue], allow_unsafe: bool) -> HashSet<String> {
1232 issues
1233 .iter()
1234 .filter_map(|issue| {
1235 let autofix = issue.autofix.as_ref()?;
1236 let applicable = match autofix.applicability {
1237 IssueAutofixApplicability::Safe => true,
1238 IssueAutofixApplicability::Unsafe => allow_unsafe,
1239 IssueAutofixApplicability::DisplayOnly => false,
1240 };
1241 if applicable && core_autofix_conflict_priority(Some(issue.code.as_str())) == 0 {
1242 Some(issue.code.clone())
1243 } else {
1244 None
1245 }
1246 })
1247 .collect()
1248}
1249
1250fn core_autofix_rules_not_improved(
1251 before_counts: &BTreeMap<String, usize>,
1252 after_counts: &BTreeMap<String, usize>,
1253 core_autofix_rules: &HashSet<String>,
1254) -> bool {
1255 let lt03_improved = after_counts
1256 .get(issue_codes::LINT_LT_003)
1257 .copied()
1258 .unwrap_or(0)
1259 < before_counts
1260 .get(issue_codes::LINT_LT_003)
1261 .copied()
1262 .unwrap_or(0);
1263
1264 core_autofix_rules.iter().any(|code| {
1265 if lt03_improved && code == issue_codes::LINT_LT_005 {
1266 return false;
1269 }
1270 let before_count = before_counts.get(code).copied().unwrap_or(0);
1271 before_count > 0 && after_counts.get(code).copied().unwrap_or(0) >= before_count
1272 })
1273}
1274
1275fn parse_errors_increased(
1276 before_counts: &BTreeMap<String, usize>,
1277 after_counts: &BTreeMap<String, usize>,
1278) -> bool {
1279 after_counts
1280 .get(issue_codes::PARSE_ERROR)
1281 .copied()
1282 .unwrap_or(0)
1283 > before_counts
1284 .get(issue_codes::PARSE_ERROR)
1285 .copied()
1286 .unwrap_or(0)
1287}
1288
1289fn regression_guard_total(counts: &BTreeMap<String, usize>) -> usize {
1290 counts.values().copied().sum()
1291}
1292
1293#[allow(clippy::too_many_arguments)]
1297fn try_fallback_fix(
1298 sql: &str,
1299 dialect: Dialect,
1300 lint_config: &LintConfig,
1301 before_counts: &BTreeMap<String, usize>,
1302 before_issues: &[Issue],
1303 core_candidates: &[FixCandidate],
1304 protected_ranges: &[PatchProtectedRange],
1305 allow_unsafe: bool,
1306 incremental_iterations: usize,
1307 incremental_rule_evaluations: usize,
1308 profile: &mut FixProfileGuard,
1309 core_label: &'static str,
1310 incremental_label: &'static str,
1311) -> Option<FixPassResult> {
1312 let stage_started = Instant::now();
1313 if let Some(outcome) = try_core_only_fix_plan(
1314 sql,
1315 dialect,
1316 lint_config,
1317 before_counts,
1318 core_candidates,
1319 protected_ranges,
1320 allow_unsafe,
1321 ) {
1322 profile.record(core_label, stage_started);
1323 return Some(FixPassResult {
1324 post_lint_state: lint_state(&outcome.sql, dialect, lint_config),
1325 outcome,
1326 });
1327 }
1328 profile.record(core_label, stage_started);
1329
1330 let stage_started = Instant::now();
1331 if let Some(outcome) = try_incremental_core_fix_plan(
1332 sql,
1333 dialect,
1334 lint_config,
1335 before_counts,
1336 Some(before_issues),
1337 allow_unsafe,
1338 incremental_iterations,
1339 incremental_rule_evaluations,
1340 ) {
1341 profile.record(incremental_label, stage_started);
1342 return Some(FixPassResult {
1343 post_lint_state: lint_state(&outcome.sql, dialect, lint_config),
1344 outcome,
1345 });
1346 }
1347 profile.record(incremental_label, stage_started);
1348
1349 None
1350}
1351
1352fn try_core_only_fix_plan(
1353 sql: &str,
1354 dialect: Dialect,
1355 lint_config: &LintConfig,
1356 before_counts: &BTreeMap<String, usize>,
1357 core_candidates: &[FixCandidate],
1358 protected_ranges: &[PatchProtectedRange],
1359 allow_unsafe: bool,
1360) -> Option<FixOutcome> {
1361 if core_candidates.is_empty() {
1362 return None;
1363 }
1364
1365 let planned = plan_fix_candidates(
1366 sql,
1367 core_candidates.to_vec(),
1368 protected_ranges,
1369 allow_unsafe,
1370 );
1371 if planned.edits.is_empty() {
1372 return None;
1373 }
1374
1375 let fixed_sql = apply_planned_edits(sql, &planned.edits);
1376 if fixed_sql == sql {
1377 return None;
1378 }
1379
1380 let after_counts = lint_rule_counts(&fixed_sql, dialect, lint_config);
1381 if parse_errors_increased(before_counts, &after_counts) {
1382 return None;
1383 }
1384
1385 let counts = FixCounts::from_removed(before_counts, &after_counts);
1386 let before_total = regression_guard_total(before_counts);
1387 let after_total = regression_guard_total(&after_counts);
1388 if counts.total() == 0 || after_total > before_total {
1389 return None;
1390 }
1391
1392 Some(FixOutcome {
1393 sql: fixed_sql,
1394 counts,
1395 changed: true,
1396 skipped_due_to_comments: false,
1397 skipped_due_to_regression: false,
1398 skipped_counts: planned.skipped,
1399 })
1400}
1401
1402fn is_incremental_core_candidate(candidate: &FixCandidate, allow_unsafe: bool) -> bool {
1403 if candidate.source != FixCandidateSource::CoreAutofix {
1404 return false;
1405 }
1406
1407 if candidate.rule_code.is_none() {
1408 return false;
1409 }
1410
1411 match candidate.applicability {
1412 FixCandidateApplicability::Safe => true,
1413 FixCandidateApplicability::Unsafe => allow_unsafe,
1414 FixCandidateApplicability::DisplayOnly => false,
1415 }
1416}
1417
1418#[derive(Debug, Clone, Copy, Eq, PartialEq)]
1419enum Al001AliasingPreference {
1420 Explicit,
1421 Implicit,
1422}
1423
1424fn al001_aliasing_preference(lint_config: &LintConfig) -> Al001AliasingPreference {
1425 if lint_config
1426 .rule_option_str(issue_codes::LINT_AL_001, "aliasing")
1427 .is_some_and(|value| value.eq_ignore_ascii_case("implicit"))
1428 {
1429 Al001AliasingPreference::Implicit
1430 } else {
1431 Al001AliasingPreference::Explicit
1432 }
1433}
1434
1435fn build_al001_fallback_candidates(
1436 sql: &str,
1437 dialect: Dialect,
1438 issues: &[Issue],
1439 lint_config: &LintConfig,
1440) -> Vec<FixCandidate> {
1441 let fallback_issues: Vec<&Issue> = issues
1442 .iter()
1443 .filter(|issue| {
1444 issue.code.eq_ignore_ascii_case(issue_codes::LINT_AL_001) && issue.span.is_some()
1445 })
1446 .collect();
1447 if fallback_issues.is_empty() {
1448 return Vec::new();
1449 }
1450
1451 let Some(tokens) = alias_tokenize_with_offsets(sql, dialect) else {
1452 return Vec::new();
1453 };
1454
1455 let preference = al001_aliasing_preference(lint_config);
1456 let mut candidates = Vec::new();
1457 for issue in fallback_issues {
1458 let Some(span) = issue.span else {
1459 continue;
1460 };
1461 let alias_start = span.start.min(sql.len());
1462 let previous_token = tokens
1463 .iter()
1464 .rev()
1465 .find(|token| token.end <= alias_start && !is_alias_trivia_token(&token.token));
1466
1467 match preference {
1468 Al001AliasingPreference::Explicit => {
1469 if previous_token.is_some_and(|token| is_as_token(&token.token)) {
1470 continue;
1471 }
1472 let replacement = if has_whitespace_before_offset(sql, alias_start) {
1473 "AS "
1474 } else {
1475 " AS "
1476 };
1477 candidates.push(FixCandidate {
1478 start: alias_start,
1479 end: alias_start,
1480 replacement: replacement.to_string(),
1481 applicability: FixCandidateApplicability::Safe,
1482 source: FixCandidateSource::CoreAutofix,
1483 rule_code: Some(issue_codes::LINT_AL_001.to_string()),
1484 });
1485 }
1486 Al001AliasingPreference::Implicit => {
1487 let Some(as_token) = previous_token.filter(|token| is_as_token(&token.token))
1488 else {
1489 continue;
1490 };
1491 candidates.push(FixCandidate {
1492 start: as_token.start,
1493 end: alias_start,
1494 replacement: " ".to_string(),
1495 applicability: FixCandidateApplicability::Safe,
1496 source: FixCandidateSource::CoreAutofix,
1497 rule_code: Some(issue_codes::LINT_AL_001.to_string()),
1498 });
1499 }
1500 }
1501 }
1502
1503 candidates
1504}
1505
1506fn merge_skipped_counts(total: &mut FixSkippedCounts, current: &FixSkippedCounts) {
1507 total.unsafe_skipped += current.unsafe_skipped;
1508 total.protected_range_blocked += current.protected_range_blocked;
1509 total.overlap_conflict_blocked += current.overlap_conflict_blocked;
1510 total.display_only += current.display_only;
1511}
1512
1513#[allow(clippy::too_many_arguments)]
1514fn try_incremental_core_fix_plan(
1515 sql: &str,
1516 dialect: Dialect,
1517 lint_config: &LintConfig,
1518 before_counts: &BTreeMap<String, usize>,
1519 initial_issues: Option<&[Issue]>,
1520 allow_unsafe: bool,
1521 max_iterations: usize,
1522 max_rule_evaluations_per_iteration: usize,
1523) -> Option<FixOutcome> {
1524 let mut current_sql = sql.to_string();
1525 let mut current_counts = before_counts.clone();
1526 let mut changed = false;
1527 let mut skipped_counts = FixSkippedCounts::default();
1528 let mut counts_cache: HashMap<String, BTreeMap<String, usize>> = HashMap::new();
1529 let mut seen_sql: HashSet<u64> = HashSet::new();
1530 seen_sql.insert(hash_sql(¤t_sql));
1531
1532 let max_iterations = max_iterations.max(1);
1533 let max_rule_evaluations_per_iteration = max_rule_evaluations_per_iteration.max(1);
1534 let mut initial_issues = initial_issues;
1535 for _ in 0..max_iterations {
1536 let issues: Cow<'_, [Issue]> = if let Some(issues) = initial_issues.take() {
1537 Cow::Borrowed(issues)
1538 } else {
1539 Cow::Owned(lint_issues(¤t_sql, dialect, lint_config))
1540 };
1541 let mut all_candidates = build_fix_candidates_from_issue_autofixes(¤t_sql, &issues);
1542 all_candidates.extend(build_al001_fallback_candidates(
1543 ¤t_sql,
1544 dialect,
1545 &issues,
1546 lint_config,
1547 ));
1548 let candidates = all_candidates
1549 .into_iter()
1550 .filter(|candidate| is_incremental_core_candidate(candidate, allow_unsafe))
1551 .collect::<Vec<_>>();
1552
1553 if candidates.is_empty() {
1554 break;
1555 }
1556
1557 let mut by_rule: BTreeMap<String, Vec<FixCandidate>> = BTreeMap::new();
1558 for candidate in candidates {
1559 if let Some(rule_code) = candidate.rule_code.clone() {
1560 by_rule.entry(rule_code).or_default().push(candidate);
1561 }
1562 }
1563
1564 if by_rule.is_empty() {
1565 break;
1566 }
1567
1568 let protected_ranges =
1569 collect_comment_protected_ranges(¤t_sql, dialect, !allow_unsafe);
1570 let current_total = regression_guard_total(¤t_counts);
1571 let mut ordered_rules = by_rule.into_iter().collect::<Vec<_>>();
1572 if max_rule_evaluations_per_iteration != usize::MAX {
1573 ordered_rules.sort_by(
1574 |(left_rule, left_candidates), (right_rule, right_candidates)| {
1575 let left_count = current_counts.get(left_rule).copied().unwrap_or(0);
1576 let right_count = current_counts.get(right_rule).copied().unwrap_or(0);
1577 right_count
1578 .cmp(&left_count)
1579 .then_with(|| right_candidates.len().cmp(&left_candidates.len()))
1580 .then_with(|| left_rule.cmp(right_rule))
1581 },
1582 );
1583 }
1584
1585 let mut best_rule: Option<String> = None;
1586 let mut best_sql: Option<String> = None;
1587 let mut best_counts: Option<BTreeMap<String, usize>> = None;
1588 let mut best_removed = 0usize;
1589 let mut best_after_total = usize::MAX;
1590 let mut evaluated_candidate_sql = HashSet::new();
1591
1592 let mut rule_evaluations = 0usize;
1593 for (rule_code, rule_candidates) in ordered_rules {
1594 if rule_evaluations >= max_rule_evaluations_per_iteration {
1595 break;
1596 }
1597 let planned = plan_fix_candidates(
1598 ¤t_sql,
1599 rule_candidates,
1600 &protected_ranges,
1601 allow_unsafe,
1602 );
1603 merge_skipped_counts(&mut skipped_counts, &planned.skipped);
1604
1605 if planned.edits.is_empty() {
1606 continue;
1607 }
1608 rule_evaluations += 1;
1609
1610 let candidate_sql = apply_planned_edits(¤t_sql, &planned.edits);
1611 if candidate_sql == current_sql {
1612 continue;
1613 }
1614 if !evaluated_candidate_sql.insert(candidate_sql.clone()) {
1615 continue;
1616 }
1617
1618 let candidate_counts = if let Some(cached) = counts_cache.get(&candidate_sql) {
1619 cached.clone()
1620 } else {
1621 let counts = lint_rule_counts(&candidate_sql, dialect, lint_config);
1622 counts_cache.insert(candidate_sql.clone(), counts.clone());
1623 counts
1624 };
1625 if parse_errors_increased(¤t_counts, &candidate_counts) {
1626 continue;
1627 }
1628
1629 let candidate_after_total = regression_guard_total(&candidate_counts);
1630 if candidate_after_total > current_total {
1631 continue;
1632 }
1633
1634 let candidate_removed =
1635 FixCounts::from_removed(¤t_counts, &candidate_counts).total();
1636 if candidate_removed == 0 {
1637 continue;
1638 }
1639
1640 let better = candidate_removed > best_removed
1641 || (candidate_removed == best_removed && candidate_after_total < best_after_total)
1642 || (candidate_removed == best_removed
1643 && candidate_after_total == best_after_total
1644 && best_rule
1645 .as_ref()
1646 .is_none_or(|current_best| rule_code < *current_best));
1647
1648 if better {
1649 best_removed = candidate_removed;
1650 best_after_total = candidate_after_total;
1651 best_rule = Some(rule_code);
1652 best_sql = Some(candidate_sql);
1653 best_counts = Some(candidate_counts);
1654 }
1655 }
1656
1657 let Some(next_sql) = best_sql else {
1658 break;
1659 };
1660 let Some(next_counts) = best_counts else {
1661 break;
1662 };
1663 if !seen_sql.insert(hash_sql(&next_sql)) {
1664 break;
1665 }
1666
1667 current_sql = next_sql;
1668 current_counts = next_counts;
1669 changed = true;
1670 }
1671
1672 if !changed || current_sql == sql {
1673 return None;
1674 }
1675
1676 let final_counts = FixCounts::from_removed(before_counts, ¤t_counts);
1677 if final_counts.total() == 0 {
1678 return None;
1679 }
1680
1681 Some(FixOutcome {
1682 sql: current_sql,
1683 counts: final_counts,
1684 changed: true,
1685 skipped_due_to_comments: false,
1686 skipped_due_to_regression: false,
1687 skipped_counts,
1688 })
1689}
1690
1691#[derive(Debug, Clone)]
1692struct TableAliasOccurrence {
1693 alias_key: String,
1694 alias_start: usize,
1695 explicit_as: bool,
1696 as_start: Option<usize>,
1697}
1698
1699fn preserve_original_table_alias_style(
1700 original_sql: &str,
1701 fixed_sql: &str,
1702 dialect: Dialect,
1703) -> String {
1704 let Some(original_aliases) = table_alias_occurrences(original_sql, dialect) else {
1705 return fixed_sql.to_string();
1706 };
1707 let Some(fixed_aliases) = table_alias_occurrences(fixed_sql, dialect) else {
1708 return fixed_sql.to_string();
1709 };
1710
1711 let mut desired_by_alias: BTreeMap<String, VecDeque<bool>> = BTreeMap::new();
1712 for alias in original_aliases {
1713 desired_by_alias
1714 .entry(alias.alias_key)
1715 .or_default()
1716 .push_back(alias.explicit_as);
1717 }
1718
1719 let mut removals = Vec::new();
1720 for alias in fixed_aliases {
1721 let desired_explicit = desired_by_alias
1722 .get_mut(&alias.alias_key)
1723 .and_then(VecDeque::pop_front)
1724 .unwrap_or(alias.explicit_as);
1725
1726 if alias.explicit_as && !desired_explicit {
1727 if let Some(as_start) = alias.as_start {
1728 removals.push((as_start, alias.alias_start));
1729 }
1730 }
1731 }
1732
1733 apply_byte_removals(fixed_sql, removals)
1734}
1735
1736fn apply_configured_table_alias_style(
1737 sql: &str,
1738 dialect: Dialect,
1739 lint_config: &LintConfig,
1740) -> String {
1741 let prefer_implicit = matches!(
1742 al001_aliasing_preference(lint_config),
1743 Al001AliasingPreference::Implicit
1744 );
1745 enforce_table_alias_style(sql, dialect, prefer_implicit)
1746}
1747
1748fn enforce_table_alias_style(sql: &str, dialect: Dialect, prefer_implicit: bool) -> String {
1749 let Some(aliases) = table_alias_occurrences(sql, dialect) else {
1750 return sql.to_string();
1751 };
1752
1753 if prefer_implicit {
1754 let removals: Vec<(usize, usize)> = aliases
1755 .into_iter()
1756 .filter_map(|alias| {
1757 if alias.explicit_as {
1758 alias.as_start.map(|as_start| (as_start, alias.alias_start))
1759 } else {
1760 None
1761 }
1762 })
1763 .collect();
1764 return apply_byte_removals(sql, removals);
1765 }
1766
1767 let insertions: Vec<(usize, &'static str)> = aliases
1768 .into_iter()
1769 .filter(|alias| !alias.explicit_as)
1770 .map(|alias| {
1771 let insertion = if has_whitespace_before_offset(sql, alias.alias_start) {
1772 "AS "
1773 } else {
1774 " AS "
1775 };
1776 (alias.alias_start, insertion)
1777 })
1778 .collect();
1779 apply_byte_insertions(sql, insertions)
1780}
1781
1782fn has_whitespace_before_offset(sql: &str, offset: usize) -> bool {
1783 sql.get(..offset)
1784 .and_then(|prefix| prefix.chars().next_back())
1785 .is_some_and(char::is_whitespace)
1786}
1787
1788fn apply_byte_removals(sql: &str, mut removals: Vec<(usize, usize)>) -> String {
1789 if removals.is_empty() {
1790 return sql.to_string();
1791 }
1792
1793 removals.sort_unstable();
1794 removals.dedup();
1795
1796 let mut out = sql.to_string();
1797 for (start, end) in removals.into_iter().rev() {
1798 if start < end && end <= out.len() {
1799 out.replace_range(start..end, "");
1800 }
1801 }
1802 out
1803}
1804
1805fn apply_byte_insertions(sql: &str, mut insertions: Vec<(usize, &'static str)>) -> String {
1806 if insertions.is_empty() {
1807 return sql.to_string();
1808 }
1809
1810 insertions.retain(|(offset, _)| *offset <= sql.len());
1811 if insertions.is_empty() {
1812 return sql.to_string();
1813 }
1814
1815 insertions
1816 .sort_unstable_by(|left, right| left.0.cmp(&right.0).then_with(|| left.1.cmp(right.1)));
1817 insertions.dedup_by(|left, right| left.0 == right.0);
1818
1819 let extra_len: usize = insertions
1820 .iter()
1821 .map(|(_, insertion)| insertion.len())
1822 .sum();
1823 let mut out = String::with_capacity(sql.len() + extra_len);
1824 let mut cursor = 0usize;
1825 for (offset, insertion) in insertions {
1826 if offset < cursor || offset > sql.len() {
1827 continue;
1828 }
1829 out.push_str(&sql[cursor..offset]);
1830 out.push_str(insertion);
1831 cursor = offset;
1832 }
1833 out.push_str(&sql[cursor..]);
1834 out
1835}
1836
1837#[derive(Debug, Clone, PartialEq, Eq)]
1838struct SpanEdit {
1839 start: usize,
1840 end: usize,
1841 replacement: String,
1842}
1843
1844impl SpanEdit {
1845 fn replace(start: usize, end: usize, replacement: impl Into<String>) -> Self {
1846 Self {
1847 start,
1848 end,
1849 replacement: replacement.into(),
1850 }
1851 }
1852}
1853
1854#[derive(Debug, Clone, Copy, Eq, PartialEq, Ord, PartialOrd)]
1855#[allow(dead_code)]
1856enum FixCandidateApplicability {
1857 Safe,
1858 Unsafe,
1859 DisplayOnly,
1860}
1861
1862impl FixCandidateApplicability {
1863 fn sort_key(self) -> u8 {
1864 match self {
1865 Self::Safe => 0,
1866 Self::Unsafe => 1,
1867 Self::DisplayOnly => 2,
1868 }
1869 }
1870}
1871
1872#[derive(Debug, Clone, Copy, Eq, PartialEq, Ord, PartialOrd)]
1873#[allow(dead_code)]
1874enum FixCandidateSource {
1875 PrimaryRewrite,
1876 CoreAutofix,
1877 UnsafeFallback,
1878 DisplayHint,
1879}
1880
1881fn core_autofix_conflict_priority(rule_code: Option<&str>) -> u8 {
1882 let Some(code) = rule_code else {
1883 return 2;
1884 };
1885
1886 if code.eq_ignore_ascii_case(issue_codes::LINT_AM_001)
1887 || code.eq_ignore_ascii_case(issue_codes::LINT_CV_001)
1888 || code.eq_ignore_ascii_case(issue_codes::LINT_AM_002)
1889 || code.eq_ignore_ascii_case(issue_codes::LINT_AM_003)
1890 || code.eq_ignore_ascii_case(issue_codes::LINT_AM_005)
1891 || code.eq_ignore_ascii_case(issue_codes::LINT_AM_008)
1892 || code.eq_ignore_ascii_case(issue_codes::LINT_CV_002)
1893 || code.eq_ignore_ascii_case(issue_codes::LINT_CV_003)
1894 || code.eq_ignore_ascii_case(issue_codes::LINT_CV_004)
1895 || code.eq_ignore_ascii_case(issue_codes::LINT_CV_005)
1896 || code.eq_ignore_ascii_case(issue_codes::LINT_CV_006)
1897 || code.eq_ignore_ascii_case(issue_codes::LINT_CV_007)
1898 || code.eq_ignore_ascii_case(issue_codes::LINT_CV_010)
1899 || code.eq_ignore_ascii_case(issue_codes::LINT_CV_012)
1900 || code.eq_ignore_ascii_case(issue_codes::LINT_CP_001)
1901 || code.eq_ignore_ascii_case(issue_codes::LINT_CP_002)
1902 || code.eq_ignore_ascii_case(issue_codes::LINT_CP_003)
1903 || code.eq_ignore_ascii_case(issue_codes::LINT_CP_004)
1904 || code.eq_ignore_ascii_case(issue_codes::LINT_CP_005)
1905 || code.eq_ignore_ascii_case(issue_codes::LINT_AL_001)
1906 || code.eq_ignore_ascii_case(issue_codes::LINT_AL_002)
1907 || code.eq_ignore_ascii_case(issue_codes::LINT_AL_005)
1908 || code.eq_ignore_ascii_case(issue_codes::LINT_AL_007)
1909 || code.eq_ignore_ascii_case(issue_codes::LINT_AL_009)
1910 || code.eq_ignore_ascii_case(issue_codes::LINT_LT_001)
1911 || code.eq_ignore_ascii_case(issue_codes::LINT_LT_002)
1912 || code.eq_ignore_ascii_case(issue_codes::LINT_LT_003)
1913 || code.eq_ignore_ascii_case(issue_codes::LINT_LT_004)
1914 || code.eq_ignore_ascii_case(issue_codes::LINT_LT_005)
1915 || code.eq_ignore_ascii_case(issue_codes::LINT_LT_006)
1916 || code.eq_ignore_ascii_case(issue_codes::LINT_LT_007)
1917 || code.eq_ignore_ascii_case(issue_codes::LINT_LT_008)
1918 || code.eq_ignore_ascii_case(issue_codes::LINT_LT_009)
1919 || code.eq_ignore_ascii_case(issue_codes::LINT_LT_010)
1920 || code.eq_ignore_ascii_case(issue_codes::LINT_LT_011)
1921 || code.eq_ignore_ascii_case(issue_codes::LINT_LT_012)
1922 || code.eq_ignore_ascii_case(issue_codes::LINT_LT_013)
1923 || code.eq_ignore_ascii_case(issue_codes::LINT_LT_014)
1924 || code.eq_ignore_ascii_case(issue_codes::LINT_LT_015)
1925 || code.eq_ignore_ascii_case(issue_codes::LINT_ST_001)
1926 || code.eq_ignore_ascii_case(issue_codes::LINT_ST_002)
1927 || code.eq_ignore_ascii_case(issue_codes::LINT_ST_006)
1928 || code.eq_ignore_ascii_case(issue_codes::LINT_ST_009)
1929 || code.eq_ignore_ascii_case(issue_codes::LINT_ST_005)
1930 || code.eq_ignore_ascii_case(issue_codes::LINT_ST_008)
1931 || code.eq_ignore_ascii_case(issue_codes::LINT_ST_012)
1932 || code.eq_ignore_ascii_case(issue_codes::LINT_TQ_002)
1933 || code.eq_ignore_ascii_case(issue_codes::LINT_TQ_003)
1934 || code.eq_ignore_ascii_case(issue_codes::LINT_RF_003)
1935 || code.eq_ignore_ascii_case(issue_codes::LINT_RF_004)
1936 || code.eq_ignore_ascii_case(issue_codes::LINT_CV_011)
1937 || code.eq_ignore_ascii_case(issue_codes::LINT_RF_006)
1938 || code.eq_ignore_ascii_case(issue_codes::LINT_JJ_001)
1939 {
1940 0
1941 } else {
1942 2
1943 }
1944}
1945
1946#[derive(Debug, Clone)]
1947struct FixCandidate {
1948 start: usize,
1949 end: usize,
1950 replacement: String,
1951 applicability: FixCandidateApplicability,
1952 source: FixCandidateSource,
1953 rule_code: Option<String>,
1954}
1955
1956fn fix_candidate_source_priority(candidate: &FixCandidate) -> u8 {
1957 match candidate.source {
1958 FixCandidateSource::CoreAutofix => {
1959 core_autofix_conflict_priority(candidate.rule_code.as_deref())
1960 }
1961 FixCandidateSource::PrimaryRewrite => 1,
1962 FixCandidateSource::UnsafeFallback => 3,
1963 FixCandidateSource::DisplayHint => 4,
1964 }
1965}
1966
1967#[derive(Debug, Default)]
1968struct PlannedFixes {
1969 edits: Vec<PatchEdit>,
1970 skipped: FixSkippedCounts,
1971}
1972
1973fn build_fix_candidates_from_rewrite(
1974 sql: &str,
1975 rewritten_sql: &str,
1976 applicability: FixCandidateApplicability,
1977 source: FixCandidateSource,
1978) -> Vec<FixCandidate> {
1979 if sql == rewritten_sql {
1980 return Vec::new();
1981 }
1982
1983 let mut candidates = derive_localized_span_edits(sql, rewritten_sql)
1984 .into_iter()
1985 .map(|edit| FixCandidate {
1986 start: edit.start,
1987 end: edit.end,
1988 replacement: edit.replacement,
1989 applicability,
1990 source,
1991 rule_code: None,
1992 })
1993 .collect::<Vec<_>>();
1994
1995 if candidates.is_empty() {
1996 candidates.push(FixCandidate {
1997 start: 0,
1998 end: sql.len(),
1999 replacement: rewritten_sql.to_string(),
2000 applicability,
2001 source,
2002 rule_code: None,
2003 });
2004 }
2005
2006 candidates
2007}
2008
2009fn build_fix_candidates_from_issue_autofixes(sql: &str, issues: &[Issue]) -> Vec<FixCandidate> {
2010 let issue_values: Vec<serde_json::Value> = issues
2011 .iter()
2012 .filter_map(|issue| serde_json::to_value(issue).ok())
2013 .collect();
2014 build_fix_candidates_from_issue_values(sql, &issue_values)
2015}
2016
2017fn build_fix_candidates_from_issue_values(
2018 sql: &str,
2019 issue_values: &[serde_json::Value],
2020) -> Vec<FixCandidate> {
2021 let mut candidates = Vec::new();
2022 let sql_len = sql.len();
2023
2024 for issue in issue_values {
2025 let fallback_span = issue.get("span").and_then(json_span_offsets);
2026 let issue_rule_code = issue
2027 .get("code")
2028 .and_then(serde_json::Value::as_str)
2029 .map(|code| code.to_string());
2030 if issue_rule_code
2031 .as_deref()
2032 .is_some_and(|code| code.eq_ignore_ascii_case(issue_codes::LINT_AL_001))
2033 {
2034 continue;
2037 }
2038 let Some(autofix) = issue.get("autofix").or_else(|| issue.get("autoFix")) else {
2039 continue;
2040 };
2041 collect_issue_autofix_candidates(
2042 autofix,
2043 fallback_span,
2044 sql_len,
2045 None,
2046 &issue_rule_code,
2047 &mut candidates,
2048 );
2049 }
2050
2051 candidates
2052}
2053
2054fn collect_issue_autofix_candidates(
2055 value: &serde_json::Value,
2056 fallback_span: Option<(usize, usize)>,
2057 sql_len: usize,
2058 inherited_applicability: Option<FixCandidateApplicability>,
2059 issue_rule_code: &Option<String>,
2060 out: &mut Vec<FixCandidate>,
2061) {
2062 match value {
2063 serde_json::Value::Array(items) => {
2064 for item in items {
2065 collect_issue_autofix_candidates(
2066 item,
2067 fallback_span,
2068 sql_len,
2069 inherited_applicability,
2070 issue_rule_code,
2071 out,
2072 );
2073 }
2074 }
2075 serde_json::Value::Object(_) => {
2076 let applicability = parse_issue_autofix_applicability(value)
2077 .or(inherited_applicability)
2078 .unwrap_or(FixCandidateApplicability::Safe);
2079
2080 if let Some(edit) = value.get("edit") {
2081 collect_issue_autofix_candidates(
2082 edit,
2083 fallback_span,
2084 sql_len,
2085 Some(applicability),
2086 issue_rule_code,
2087 out,
2088 );
2089 }
2090 if let Some(edits) = value
2091 .get("edits")
2092 .or_else(|| value.get("fixes"))
2093 .or_else(|| value.get("changes"))
2094 {
2095 collect_issue_autofix_candidates(
2096 edits,
2097 fallback_span,
2098 sql_len,
2099 Some(applicability),
2100 issue_rule_code,
2101 out,
2102 );
2103 }
2104
2105 if let Some((start, end)) = parse_issue_autofix_offsets(value, fallback_span) {
2106 if start <= end
2107 && end <= sql_len
2108 && value
2109 .get("replacement")
2110 .or_else(|| value.get("new_text"))
2111 .or_else(|| value.get("newText"))
2112 .or_else(|| value.get("text"))
2113 .and_then(serde_json::Value::as_str)
2114 .is_some()
2115 {
2116 let replacement = value
2117 .get("replacement")
2118 .or_else(|| value.get("new_text"))
2119 .or_else(|| value.get("newText"))
2120 .or_else(|| value.get("text"))
2121 .and_then(serde_json::Value::as_str)
2122 .unwrap_or_default()
2123 .to_string();
2124
2125 out.push(FixCandidate {
2126 start,
2127 end,
2128 replacement,
2129 applicability,
2130 source: FixCandidateSource::CoreAutofix,
2131 rule_code: issue_rule_code.clone(),
2132 });
2133 }
2134 }
2135 }
2136 _ => {}
2137 }
2138}
2139
2140fn parse_issue_autofix_offsets(
2141 value: &serde_json::Value,
2142 fallback_span: Option<(usize, usize)>,
2143) -> Option<(usize, usize)> {
2144 let object = value.as_object()?;
2145
2146 let mut start = json_usize_field(object, &["start", "start_byte", "startByte"]);
2147 let mut end = json_usize_field(object, &["end", "end_byte", "endByte"]);
2148
2149 if let Some((span_start, span_end)) = object.get("span").and_then(json_span_offsets) {
2150 if start.is_none() {
2151 start = Some(span_start);
2152 }
2153 if end.is_none() {
2154 end = Some(span_end);
2155 }
2156 }
2157
2158 if let Some((span_start, span_end)) = fallback_span {
2159 if start.is_none() {
2160 start = Some(span_start);
2161 }
2162 if end.is_none() {
2163 end = Some(span_end);
2164 }
2165 }
2166
2167 Some((start?, end?))
2168}
2169
2170fn json_span_offsets(value: &serde_json::Value) -> Option<(usize, usize)> {
2171 let object = value.as_object()?;
2172 let start = json_usize_field(object, &["start", "start_byte", "startByte"])?;
2173 let end = json_usize_field(object, &["end", "end_byte", "endByte"])?;
2174 Some((start, end))
2175}
2176
2177fn json_usize_field(
2178 object: &serde_json::Map<String, serde_json::Value>,
2179 keys: &[&str],
2180) -> Option<usize> {
2181 keys.iter().find_map(|key| {
2182 object.get(*key).and_then(|value| {
2183 value
2184 .as_u64()
2185 .and_then(|raw| usize::try_from(raw).ok())
2186 .or_else(|| value.as_str().and_then(|raw| raw.parse::<usize>().ok()))
2187 })
2188 })
2189}
2190
2191fn parse_issue_autofix_applicability(
2192 value: &serde_json::Value,
2193) -> Option<FixCandidateApplicability> {
2194 let object = value.as_object()?;
2195
2196 if object
2197 .get("display_only")
2198 .or_else(|| object.get("displayOnly"))
2199 .and_then(serde_json::Value::as_bool)
2200 == Some(true)
2201 {
2202 return Some(FixCandidateApplicability::DisplayOnly);
2203 }
2204 if object.get("unsafe").and_then(serde_json::Value::as_bool) == Some(true) {
2205 return Some(FixCandidateApplicability::Unsafe);
2206 }
2207
2208 let text = object
2209 .get("applicability")
2210 .or_else(|| object.get("safety"))
2211 .or_else(|| object.get("kind"))
2212 .or_else(|| object.get("mode"))
2213 .and_then(serde_json::Value::as_str)?;
2214 parse_issue_autofix_applicability_text(text)
2215}
2216
2217fn parse_issue_autofix_applicability_text(text: &str) -> Option<FixCandidateApplicability> {
2218 match text.trim().to_ascii_lowercase().as_str() {
2219 "safe" => Some(FixCandidateApplicability::Safe),
2220 "unsafe" => Some(FixCandidateApplicability::Unsafe),
2221 "display_only" | "display-only" | "displayonly" | "display" | "hint" | "suggestion" => {
2222 Some(FixCandidateApplicability::DisplayOnly)
2223 }
2224 _ => None,
2225 }
2226}
2227
2228fn plan_fix_candidates(
2229 sql: &str,
2230 mut candidates: Vec<FixCandidate>,
2231 protected_ranges: &[PatchProtectedRange],
2232 allow_unsafe: bool,
2233) -> PlannedFixes {
2234 if candidates.is_empty() {
2235 return PlannedFixes::default();
2236 }
2237
2238 candidates.sort_by(|left, right| {
2239 left.start
2240 .cmp(&right.start)
2241 .then_with(|| left.end.cmp(&right.end))
2242 .then_with(|| {
2243 left.applicability
2244 .sort_key()
2245 .cmp(&right.applicability.sort_key())
2246 })
2247 .then_with(|| {
2248 fix_candidate_source_priority(left).cmp(&fix_candidate_source_priority(right))
2249 })
2250 .then_with(|| left.rule_code.cmp(&right.rule_code))
2251 .then_with(|| left.replacement.cmp(&right.replacement))
2252 });
2253 candidates.dedup_by(|left, right| {
2254 left.start == right.start
2255 && left.end == right.end
2256 && left.replacement == right.replacement
2257 && left.applicability == right.applicability
2258 && left.source == right.source
2259 && left.rule_code == right.rule_code
2260 });
2261
2262 let patch_fixes: Vec<PatchFix> = candidates
2263 .into_iter()
2264 .enumerate()
2265 .map(|(idx, candidate)| {
2266 let rule_code = candidate
2267 .rule_code
2268 .clone()
2269 .unwrap_or_else(|| format!("PATCH_{:?}_{idx}", candidate.source));
2270 let source_priority = fix_candidate_source_priority(&candidate);
2271 let mut fix = PatchFix::new(
2272 rule_code,
2273 patch_applicability(candidate.applicability),
2274 vec![PatchEdit::replace(
2275 candidate.start,
2276 candidate.end,
2277 candidate.replacement,
2278 )],
2279 );
2280 fix.priority = source_priority as i32;
2281 fix
2282 })
2283 .collect();
2284
2285 let mut allowed = vec![PatchApplicability::Safe];
2286 if allow_unsafe {
2287 allowed.push(PatchApplicability::Unsafe);
2288 }
2289
2290 let plan = plan_fixes(sql, patch_fixes, &allowed, protected_ranges);
2291 let mut skipped = FixSkippedCounts::default();
2292 for blocked in &plan.blocked {
2293 let reasons = &blocked.reasons;
2294 if reasons.iter().any(|reason| {
2295 matches!(
2296 reason,
2297 BlockedReason::ApplicabilityNotAllowed {
2298 applicability: PatchApplicability::Unsafe
2299 }
2300 )
2301 }) {
2302 skipped.unsafe_skipped += 1;
2303 continue;
2304 }
2305 if reasons.iter().any(|reason| {
2306 matches!(
2307 reason,
2308 BlockedReason::ApplicabilityNotAllowed {
2309 applicability: PatchApplicability::DisplayOnly
2310 }
2311 )
2312 }) {
2313 skipped.display_only += 1;
2314 continue;
2315 }
2316 if reasons
2317 .iter()
2318 .any(|reason| matches!(reason, BlockedReason::TouchesProtectedRange { .. }))
2319 {
2320 skipped.protected_range_blocked += 1;
2321 continue;
2322 }
2323 skipped.overlap_conflict_blocked += 1;
2324 }
2325
2326 PlannedFixes {
2327 edits: plan.accepted_edits(),
2328 skipped,
2329 }
2330}
2331
2332fn patch_applicability(applicability: FixCandidateApplicability) -> PatchApplicability {
2333 match applicability {
2334 FixCandidateApplicability::Safe => PatchApplicability::Safe,
2335 FixCandidateApplicability::Unsafe => PatchApplicability::Unsafe,
2336 FixCandidateApplicability::DisplayOnly => PatchApplicability::DisplayOnly,
2337 }
2338}
2339
2340fn apply_planned_edits(sql: &str, edits: &[PatchEdit]) -> String {
2341 apply_patch_edits(sql, edits)
2342}
2343
2344fn collect_comment_protected_ranges(
2345 sql: &str,
2346 dialect: Dialect,
2347 strict_safety_mode: bool,
2348) -> Vec<PatchProtectedRange> {
2349 if !strict_safety_mode {
2350 return Vec::new();
2351 }
2352
2353 derive_protected_ranges(sql, dialect)
2354 .into_iter()
2355 .filter(|range| matches!(range.kind, PatchProtectedRangeKind::TemplateTag))
2356 .collect()
2357}
2358
2359fn derive_localized_span_edits(original: &str, rewritten: &str) -> Vec<SpanEdit> {
2360 if original == rewritten {
2361 return Vec::new();
2362 }
2363
2364 let original_chars = original.chars().collect::<Vec<_>>();
2365 let rewritten_chars = rewritten.chars().collect::<Vec<_>>();
2366
2367 const MAX_DIFF_MATRIX_CELLS: usize = 2_500_000;
2368 let matrix_cells = (original_chars.len() + 1).saturating_mul(rewritten_chars.len() + 1);
2369 if matrix_cells > MAX_DIFF_MATRIX_CELLS {
2370 return vec![SpanEdit::replace(0, original.len(), rewritten)];
2371 }
2372
2373 let diff_steps = diff_steps_via_lcs(&original_chars, &rewritten_chars);
2374 if diff_steps.is_empty() {
2375 return Vec::new();
2376 }
2377
2378 let original_offsets = char_to_byte_offsets(original);
2379 let rewritten_offsets = char_to_byte_offsets(rewritten);
2380
2381 let mut edits = Vec::new();
2382 let mut original_char_idx = 0usize;
2383 let mut rewritten_char_idx = 0usize;
2384 let mut step_idx = 0usize;
2385
2386 while step_idx < diff_steps.len() {
2387 if matches!(diff_steps[step_idx], DiffStep::Equal) {
2388 original_char_idx += 1;
2389 rewritten_char_idx += 1;
2390 step_idx += 1;
2391 continue;
2392 }
2393
2394 let edit_original_start = original_char_idx;
2395 let edit_rewritten_start = rewritten_char_idx;
2396
2397 while step_idx < diff_steps.len() && !matches!(diff_steps[step_idx], DiffStep::Equal) {
2398 match diff_steps[step_idx] {
2399 DiffStep::Delete => original_char_idx += 1,
2400 DiffStep::Insert => rewritten_char_idx += 1,
2401 DiffStep::Equal => {}
2402 }
2403 step_idx += 1;
2404 }
2405
2406 let start = original_offsets[edit_original_start];
2407 let end = original_offsets[original_char_idx];
2408 let replacement_start = rewritten_offsets[edit_rewritten_start];
2409 let replacement_end = rewritten_offsets[rewritten_char_idx];
2410 edits.push(SpanEdit::replace(
2411 start,
2412 end,
2413 &rewritten[replacement_start..replacement_end],
2414 ));
2415 }
2416
2417 edits
2418}
2419
2420#[derive(Debug, Clone, Copy, Eq, PartialEq)]
2421enum DiffStep {
2422 Equal,
2423 Delete,
2424 Insert,
2425}
2426
2427fn diff_steps_via_lcs(original: &[char], rewritten: &[char]) -> Vec<DiffStep> {
2428 if original.is_empty() {
2429 return vec![DiffStep::Insert; rewritten.len()];
2430 }
2431 if rewritten.is_empty() {
2432 return vec![DiffStep::Delete; original.len()];
2433 }
2434
2435 let cols = rewritten.len() + 1;
2436 let mut lcs = vec![0u32; (original.len() + 1) * cols];
2437
2438 for original_idx in 0..original.len() {
2439 for rewritten_idx in 0..rewritten.len() {
2440 let cell = (original_idx + 1) * cols + rewritten_idx + 1;
2441 lcs[cell] = if original[original_idx] == rewritten[rewritten_idx] {
2442 lcs[original_idx * cols + rewritten_idx] + 1
2443 } else {
2444 lcs[original_idx * cols + rewritten_idx + 1]
2445 .max(lcs[(original_idx + 1) * cols + rewritten_idx])
2446 };
2447 }
2448 }
2449
2450 let mut steps_reversed = Vec::with_capacity(original.len() + rewritten.len());
2451 let mut original_idx = original.len();
2452 let mut rewritten_idx = rewritten.len();
2453
2454 while original_idx > 0 || rewritten_idx > 0 {
2455 if original_idx > 0
2456 && rewritten_idx > 0
2457 && original[original_idx - 1] == rewritten[rewritten_idx - 1]
2458 {
2459 steps_reversed.push(DiffStep::Equal);
2460 original_idx -= 1;
2461 rewritten_idx -= 1;
2462 continue;
2463 }
2464
2465 let left = if rewritten_idx > 0 {
2466 lcs[original_idx * cols + rewritten_idx - 1]
2467 } else {
2468 0
2469 };
2470 let up = if original_idx > 0 {
2471 lcs[(original_idx - 1) * cols + rewritten_idx]
2472 } else {
2473 0
2474 };
2475
2476 if rewritten_idx > 0 && (original_idx == 0 || left >= up) {
2477 steps_reversed.push(DiffStep::Insert);
2478 rewritten_idx -= 1;
2479 } else if original_idx > 0 {
2480 steps_reversed.push(DiffStep::Delete);
2481 original_idx -= 1;
2482 }
2483 }
2484
2485 steps_reversed.reverse();
2486 steps_reversed
2487}
2488
2489fn char_to_byte_offsets(text: &str) -> Vec<usize> {
2490 let mut offsets = Vec::with_capacity(text.chars().count() + 1);
2491 offsets.push(0);
2492 for (idx, ch) in text.char_indices() {
2493 offsets.push(idx + ch.len_utf8());
2494 }
2495 offsets
2496}
2497
2498fn table_alias_occurrences(sql: &str, dialect: Dialect) -> Option<Vec<TableAliasOccurrence>> {
2499 let statements = parse_sql_with_dialect(sql, dialect).ok()?;
2500 let tokens = alias_tokenize_with_offsets(sql, dialect)?;
2501
2502 let mut aliases = Vec::new();
2503 for statement in &statements {
2504 collect_table_alias_idents_in_statement(statement, &mut |ident| {
2505 aliases.push(ident.clone())
2506 });
2507 }
2508
2509 let mut occurrences = Vec::with_capacity(aliases.len());
2510 for alias in aliases {
2511 let Some((alias_start, _alias_end)) = alias_ident_span_offsets(sql, &alias) else {
2512 continue;
2513 };
2514 let previous_token = tokens
2515 .iter()
2516 .rev()
2517 .find(|token| token.end <= alias_start && !is_alias_trivia_token(&token.token));
2518
2519 let (explicit_as, as_start) = match previous_token {
2520 Some(token) if is_as_token(&token.token) => (true, Some(token.start)),
2521 _ => (false, None),
2522 };
2523
2524 occurrences.push(TableAliasOccurrence {
2525 alias_key: alias.value.to_ascii_lowercase(),
2526 alias_start,
2527 explicit_as,
2528 as_start,
2529 });
2530 }
2531
2532 Some(occurrences)
2533}
2534
2535fn alias_ident_span_offsets(sql: &str, ident: &Ident) -> Option<(usize, usize)> {
2536 let start = alias_line_col_to_offset(
2537 sql,
2538 ident.span.start.line as usize,
2539 ident.span.start.column as usize,
2540 )?;
2541 let end = alias_line_col_to_offset(
2542 sql,
2543 ident.span.end.line as usize,
2544 ident.span.end.column as usize,
2545 )?;
2546 Some((start, end))
2547}
2548
2549fn is_as_token(token: &Token) -> bool {
2550 matches!(token, Token::Word(word) if word.value.eq_ignore_ascii_case("AS"))
2551}
2552
2553#[derive(Clone)]
2554struct AliasLocatedToken {
2555 token: Token,
2556 start: usize,
2557 end: usize,
2558}
2559
2560fn alias_tokenize_with_offsets(sql: &str, dialect: Dialect) -> Option<Vec<AliasLocatedToken>> {
2561 let dialect = dialect.to_sqlparser_dialect();
2562 let mut tokenizer = Tokenizer::new(dialect.as_ref(), sql);
2563 let tokens = tokenizer.tokenize_with_location().ok()?;
2564
2565 let mut out = Vec::with_capacity(tokens.len());
2566 for token in tokens {
2567 let Some((start, end)) = alias_token_with_span_offsets(sql, &token) else {
2568 continue;
2569 };
2570 out.push(AliasLocatedToken {
2571 token: token.token,
2572 start,
2573 end,
2574 });
2575 }
2576
2577 Some(out)
2578}
2579
2580fn alias_token_with_span_offsets(sql: &str, token: &TokenWithSpan) -> Option<(usize, usize)> {
2581 let start = alias_line_col_to_offset(
2582 sql,
2583 token.span.start.line as usize,
2584 token.span.start.column as usize,
2585 )?;
2586 let end = alias_line_col_to_offset(
2587 sql,
2588 token.span.end.line as usize,
2589 token.span.end.column as usize,
2590 )?;
2591 Some((start, end))
2592}
2593
2594fn alias_line_col_to_offset(sql: &str, line: usize, column: usize) -> Option<usize> {
2595 if line == 0 || column == 0 {
2596 return None;
2597 }
2598
2599 let mut current_line = 1usize;
2600 let mut current_col = 1usize;
2601 for (offset, ch) in sql.char_indices() {
2602 if current_line == line && current_col == column {
2603 return Some(offset);
2604 }
2605 if ch == '\n' {
2606 current_line += 1;
2607 current_col = 1;
2608 } else {
2609 current_col += 1;
2610 }
2611 }
2612 if current_line == line && current_col == column {
2613 return Some(sql.len());
2614 }
2615 None
2616}
2617
2618fn is_alias_trivia_token(token: &Token) -> bool {
2619 matches!(
2620 token,
2621 Token::Whitespace(
2622 Whitespace::Space
2623 | Whitespace::Newline
2624 | Whitespace::Tab
2625 | Whitespace::SingleLineComment { .. }
2626 | Whitespace::MultiLineComment(_)
2627 )
2628 )
2629}
2630
2631fn collect_table_alias_idents_in_statement<F: FnMut(&Ident)>(
2632 statement: &Statement,
2633 visitor: &mut F,
2634) {
2635 match statement {
2636 Statement::Query(query) => collect_table_alias_idents_in_query(query, visitor),
2637 Statement::Insert(insert) => {
2638 if let Some(source) = &insert.source {
2639 collect_table_alias_idents_in_query(source, visitor);
2640 }
2641 }
2642 Statement::CreateView { query, .. } => collect_table_alias_idents_in_query(query, visitor),
2643 Statement::CreateTable(create) => {
2644 if let Some(query) = &create.query {
2645 collect_table_alias_idents_in_query(query, visitor);
2646 }
2647 }
2648 Statement::Merge { table, source, .. } => {
2649 collect_table_alias_idents_in_table_factor(table, visitor);
2650 collect_table_alias_idents_in_table_factor(source, visitor);
2651 }
2652 _ => {}
2653 }
2654}
2655
2656fn collect_table_alias_idents_in_query<F: FnMut(&Ident)>(query: &Query, visitor: &mut F) {
2657 if let Some(with) = &query.with {
2658 for cte in &with.cte_tables {
2659 collect_table_alias_idents_in_query(&cte.query, visitor);
2660 }
2661 }
2662
2663 collect_table_alias_idents_in_set_expr(&query.body, visitor);
2664}
2665
2666fn collect_table_alias_idents_in_set_expr<F: FnMut(&Ident)>(set_expr: &SetExpr, visitor: &mut F) {
2667 match set_expr {
2668 SetExpr::Select(select) => {
2669 for table in &select.from {
2670 collect_table_alias_idents_in_table_with_joins(table, visitor);
2671 }
2672 }
2673 SetExpr::Query(query) => collect_table_alias_idents_in_query(query, visitor),
2674 SetExpr::SetOperation { left, right, .. } => {
2675 collect_table_alias_idents_in_set_expr(left, visitor);
2676 collect_table_alias_idents_in_set_expr(right, visitor);
2677 }
2678 SetExpr::Insert(statement)
2679 | SetExpr::Update(statement)
2680 | SetExpr::Delete(statement)
2681 | SetExpr::Merge(statement) => collect_table_alias_idents_in_statement(statement, visitor),
2682 _ => {}
2683 }
2684}
2685
2686fn collect_table_alias_idents_in_table_with_joins<F: FnMut(&Ident)>(
2687 table_with_joins: &TableWithJoins,
2688 visitor: &mut F,
2689) {
2690 collect_table_alias_idents_in_table_factor(&table_with_joins.relation, visitor);
2691 for join in &table_with_joins.joins {
2692 collect_table_alias_idents_in_table_factor(&join.relation, visitor);
2693 }
2694}
2695
2696fn collect_table_alias_idents_in_table_factor<F: FnMut(&Ident)>(
2697 table_factor: &TableFactor,
2698 visitor: &mut F,
2699) {
2700 if let Some(alias) = table_factor_alias_ident(table_factor) {
2701 visitor(alias);
2702 }
2703
2704 match table_factor {
2705 TableFactor::Derived { subquery, .. } => {
2706 collect_table_alias_idents_in_query(subquery, visitor)
2707 }
2708 TableFactor::NestedJoin {
2709 table_with_joins, ..
2710 } => collect_table_alias_idents_in_table_with_joins(table_with_joins, visitor),
2711 TableFactor::Pivot { table, .. }
2712 | TableFactor::Unpivot { table, .. }
2713 | TableFactor::MatchRecognize { table, .. } => {
2714 collect_table_alias_idents_in_table_factor(table, visitor)
2715 }
2716 _ => {}
2717 }
2718}
2719
2720#[cfg(test)]
2721fn is_ascii_whitespace_byte(byte: u8) -> bool {
2722 matches!(byte, b' ' | b'\n' | b'\r' | b'\t' | 0x0b | 0x0c)
2723}
2724
2725#[cfg(test)]
2726fn is_ascii_ident_start(byte: u8) -> bool {
2727 byte.is_ascii_alphabetic() || byte == b'_'
2728}
2729
2730#[cfg(test)]
2731fn is_ascii_ident_continue(byte: u8) -> bool {
2732 byte.is_ascii_alphanumeric() || byte == b'_'
2733}
2734
2735#[cfg(test)]
2736fn skip_ascii_whitespace(bytes: &[u8], mut idx: usize) -> usize {
2737 while idx < bytes.len() && is_ascii_whitespace_byte(bytes[idx]) {
2738 idx += 1;
2739 }
2740 idx
2741}
2742
2743#[cfg(test)]
2744fn consume_ascii_identifier(bytes: &[u8], start: usize) -> Option<usize> {
2745 if start >= bytes.len() || !is_ascii_ident_start(bytes[start]) {
2746 return None;
2747 }
2748 let mut idx = start + 1;
2749 while idx < bytes.len() && is_ascii_ident_continue(bytes[idx]) {
2750 idx += 1;
2751 }
2752 Some(idx)
2753}
2754
2755#[cfg(test)]
2756fn is_word_boundary_for_keyword(bytes: &[u8], idx: usize) -> bool {
2757 idx == 0 || idx >= bytes.len() || !is_ascii_ident_continue(bytes[idx])
2758}
2759
2760#[cfg(test)]
2761fn match_ascii_keyword_at(bytes: &[u8], start: usize, keyword_upper: &[u8]) -> Option<usize> {
2762 let end = start.checked_add(keyword_upper.len())?;
2763 if end > bytes.len() {
2764 return None;
2765 }
2766 if !is_word_boundary_for_keyword(bytes, start.saturating_sub(1))
2767 || !is_word_boundary_for_keyword(bytes, end)
2768 {
2769 return None;
2770 }
2771 let matches = bytes[start..end]
2772 .iter()
2773 .zip(keyword_upper.iter())
2774 .all(|(actual, expected)| actual.to_ascii_uppercase() == *expected);
2775 if matches {
2776 Some(end)
2777 } else {
2778 None
2779 }
2780}
2781
2782#[cfg(test)]
2783fn parse_subquery_alias_suffix(suffix: &str) -> Option<String> {
2784 let bytes = suffix.as_bytes();
2785 let mut i = skip_ascii_whitespace(bytes, 0);
2786 if let Some(as_end) = match_ascii_keyword_at(bytes, i, b"AS") {
2787 let after_as = skip_ascii_whitespace(bytes, as_end);
2788 if after_as == as_end {
2789 return None;
2790 }
2791 i = after_as;
2792 }
2793
2794 let alias_start = i;
2795 let alias_end = consume_ascii_identifier(bytes, alias_start)?;
2796 i = skip_ascii_whitespace(bytes, alias_end);
2797 if i < bytes.len() && bytes[i] == b';' {
2798 i += 1;
2799 i = skip_ascii_whitespace(bytes, i);
2800 }
2801 if i != bytes.len() {
2802 return None;
2803 }
2804 Some(suffix[alias_start..alias_end].to_string())
2805}
2806
2807#[cfg(test)]
2808fn fix_subquery_to_cte(sql: &str) -> String {
2809 let bytes = sql.as_bytes();
2810 let mut i = skip_ascii_whitespace(bytes, 0);
2811 let Some(select_end) = match_ascii_keyword_at(bytes, i, b"SELECT") else {
2812 return sql.to_string();
2813 };
2814 i = skip_ascii_whitespace(bytes, select_end);
2815 if i == select_end || i >= bytes.len() || bytes[i] != b'*' {
2816 return sql.to_string();
2817 }
2818 i += 1;
2819 let from_start = skip_ascii_whitespace(bytes, i);
2820 if from_start == i {
2821 return sql.to_string();
2822 }
2823 let Some(from_end) = match_ascii_keyword_at(bytes, from_start, b"FROM") else {
2824 return sql.to_string();
2825 };
2826 let open_paren_idx = skip_ascii_whitespace(bytes, from_end);
2827 if open_paren_idx == from_end || open_paren_idx >= bytes.len() || bytes[open_paren_idx] != b'('
2828 {
2829 return sql.to_string();
2830 };
2831
2832 let Some(close_paren_idx) = find_matching_parenthesis_outside_quotes(sql, open_paren_idx)
2833 else {
2834 return sql.to_string();
2835 };
2836
2837 let subquery = sql[open_paren_idx + 1..close_paren_idx].trim();
2838 if !subquery.to_ascii_lowercase().starts_with("select") {
2839 return sql.to_string();
2840 }
2841
2842 let suffix = &sql[close_paren_idx + 1..];
2843 let Some(alias) = parse_subquery_alias_suffix(suffix) else {
2844 return sql.to_string();
2845 };
2846
2847 let mut rewritten = format!("WITH {alias} AS ({subquery}) SELECT * FROM {alias}");
2848 if suffix.trim_end().ends_with(';') {
2849 rewritten.push(';');
2850 }
2851 rewritten
2852}
2853
2854#[cfg(test)]
2855fn find_matching_parenthesis_outside_quotes(sql: &str, open_paren_idx: usize) -> Option<usize> {
2856 #[derive(Clone, Copy, PartialEq, Eq)]
2857 enum Mode {
2858 Outside,
2859 SingleQuote,
2860 DoubleQuote,
2861 BacktickQuote,
2862 BracketQuote,
2863 }
2864
2865 let bytes = sql.as_bytes();
2866 if open_paren_idx >= bytes.len() || bytes[open_paren_idx] != b'(' {
2867 return None;
2868 }
2869
2870 let mut depth = 0usize;
2871 let mut mode = Mode::Outside;
2872 let mut i = open_paren_idx;
2873
2874 while i < bytes.len() {
2875 let b = bytes[i];
2876 let next = bytes.get(i + 1).copied();
2877
2878 match mode {
2879 Mode::Outside => {
2880 if b == b'\'' {
2881 mode = Mode::SingleQuote;
2882 i += 1;
2883 continue;
2884 }
2885 if b == b'"' {
2886 mode = Mode::DoubleQuote;
2887 i += 1;
2888 continue;
2889 }
2890 if b == b'`' {
2891 mode = Mode::BacktickQuote;
2892 i += 1;
2893 continue;
2894 }
2895 if b == b'[' {
2896 mode = Mode::BracketQuote;
2897 i += 1;
2898 continue;
2899 }
2900 if b == b'(' {
2901 depth += 1;
2902 i += 1;
2903 continue;
2904 }
2905 if b == b')' {
2906 depth = depth.checked_sub(1)?;
2907 if depth == 0 {
2908 return Some(i);
2909 }
2910 }
2911 i += 1;
2912 }
2913 Mode::SingleQuote => {
2914 if b == b'\'' {
2915 if next == Some(b'\'') {
2916 i += 2;
2917 } else {
2918 mode = Mode::Outside;
2919 i += 1;
2920 }
2921 } else {
2922 i += 1;
2923 }
2924 }
2925 Mode::DoubleQuote => {
2926 if b == b'"' {
2927 if next == Some(b'"') {
2928 i += 2;
2929 } else {
2930 mode = Mode::Outside;
2931 i += 1;
2932 }
2933 } else {
2934 i += 1;
2935 }
2936 }
2937 Mode::BacktickQuote => {
2938 if b == b'`' {
2939 if next == Some(b'`') {
2940 i += 2;
2941 } else {
2942 mode = Mode::Outside;
2943 i += 1;
2944 }
2945 } else {
2946 i += 1;
2947 }
2948 }
2949 Mode::BracketQuote => {
2950 if b == b']' {
2951 if next == Some(b']') {
2952 i += 2;
2953 } else {
2954 mode = Mode::Outside;
2955 i += 1;
2956 }
2957 } else {
2958 i += 1;
2959 }
2960 }
2961 }
2962 }
2963
2964 None
2965}
2966
2967fn fix_statement(stmt: &mut Statement, rule_filter: &RuleFilter) {
2968 match stmt {
2969 Statement::Query(query) => fix_query(query, rule_filter),
2970 Statement::Insert(insert) => {
2971 if let Some(source) = insert.source.as_mut() {
2972 fix_query(source, rule_filter);
2973 }
2974 }
2975 Statement::CreateView { query, .. } => fix_query(query, rule_filter),
2976 Statement::CreateTable(create) => {
2977 if let Some(query) = create.query.as_mut() {
2978 fix_query(query, rule_filter);
2979 }
2980 }
2981 _ => {}
2982 }
2983}
2984
2985fn fix_query(query: &mut Query, rule_filter: &RuleFilter) {
2986 if let Some(with) = query.with.as_mut() {
2987 for cte in &mut with.cte_tables {
2988 fix_query(&mut cte.query, rule_filter);
2989 }
2990 }
2991
2992 fix_set_expr(query.body.as_mut(), rule_filter);
2993 rewrite_simple_derived_subqueries_to_ctes(query, rule_filter);
2994
2995 if let Some(order_by) = query.order_by.as_mut() {
2996 fix_order_by(order_by, rule_filter);
2997 }
2998
2999 if let Some(limit_clause) = query.limit_clause.as_mut() {
3000 fix_limit_clause(limit_clause, rule_filter);
3001 }
3002
3003 if let Some(fetch) = query.fetch.as_mut() {
3004 if let Some(quantity) = fetch.quantity.as_mut() {
3005 fix_expr(quantity, rule_filter);
3006 }
3007 }
3008}
3009
3010fn fix_set_expr(body: &mut SetExpr, rule_filter: &RuleFilter) {
3011 match body {
3012 SetExpr::Select(select) => fix_select(select, rule_filter),
3013 SetExpr::Query(query) => fix_query(query, rule_filter),
3014 SetExpr::SetOperation { left, right, .. } => {
3015 fix_set_expr(left, rule_filter);
3016 fix_set_expr(right, rule_filter);
3017 }
3018 SetExpr::Values(values) => {
3019 for row in &mut values.rows {
3020 for expr in row {
3021 fix_expr(expr, rule_filter);
3022 }
3023 }
3024 }
3025 SetExpr::Insert(stmt)
3026 | SetExpr::Update(stmt)
3027 | SetExpr::Delete(stmt)
3028 | SetExpr::Merge(stmt) => fix_statement(stmt, rule_filter),
3029 _ => {}
3030 }
3031}
3032
3033fn fix_select(select: &mut Select, rule_filter: &RuleFilter) {
3034 for item in &mut select.projection {
3035 match item {
3036 SelectItem::UnnamedExpr(expr) => {
3037 fix_expr(expr, rule_filter);
3038 }
3039 SelectItem::ExprWithAlias { expr, .. } => {
3040 fix_expr(expr, rule_filter);
3041 }
3042 SelectItem::QualifiedWildcard(SelectItemQualifiedWildcardKind::Expr(expr), _) => {
3043 fix_expr(expr, rule_filter);
3044 }
3045 _ => {}
3046 }
3047 }
3048
3049 for table_with_joins in &mut select.from {
3050 if rule_filter.allows(issue_codes::LINT_CV_008) {
3051 rewrite_right_join_to_left(table_with_joins);
3052 }
3053
3054 fix_table_factor(&mut table_with_joins.relation, rule_filter);
3055
3056 let mut left_ref = table_factor_reference_name(&table_with_joins.relation);
3057
3058 for join in &mut table_with_joins.joins {
3059 let right_ref = table_factor_reference_name(&join.relation);
3060 if rule_filter.allows(issue_codes::LINT_ST_007) {
3061 rewrite_using_join_constraint(
3062 &mut join.join_operator,
3063 left_ref.as_deref(),
3064 right_ref.as_deref(),
3065 );
3066 }
3067
3068 fix_table_factor(&mut join.relation, rule_filter);
3069 fix_join_operator(&mut join.join_operator, rule_filter);
3070
3071 if right_ref.is_some() {
3072 left_ref = right_ref;
3073 }
3074 }
3075 }
3076
3077 if let Some(prewhere) = select.prewhere.as_mut() {
3078 fix_expr(prewhere, rule_filter);
3079 }
3080
3081 if let Some(selection) = select.selection.as_mut() {
3082 fix_expr(selection, rule_filter);
3083 }
3084
3085 if let Some(having) = select.having.as_mut() {
3086 fix_expr(having, rule_filter);
3087 }
3088
3089 if let Some(qualify) = select.qualify.as_mut() {
3090 fix_expr(qualify, rule_filter);
3091 }
3092
3093 if let GroupByExpr::Expressions(exprs, _) = &mut select.group_by {
3094 for expr in exprs {
3095 fix_expr(expr, rule_filter);
3096 }
3097 }
3098
3099 for expr in &mut select.cluster_by {
3100 fix_expr(expr, rule_filter);
3101 }
3102
3103 for expr in &mut select.distribute_by {
3104 fix_expr(expr, rule_filter);
3105 }
3106
3107 for expr in &mut select.sort_by {
3108 fix_expr(&mut expr.expr, rule_filter);
3109 }
3110
3111 for lateral_view in &mut select.lateral_views {
3112 fix_expr(&mut lateral_view.lateral_view, rule_filter);
3113 }
3114
3115 if let Some(connect_by) = select.connect_by.as_mut() {
3116 fix_expr(&mut connect_by.condition, rule_filter);
3117 for relationship in &mut connect_by.relationships {
3118 fix_expr(relationship, rule_filter);
3119 }
3120 }
3121}
3122
3123fn rewrite_simple_derived_subqueries_to_ctes(query: &mut Query, rule_filter: &RuleFilter) {
3124 if !rule_filter.allows(issue_codes::LINT_ST_005) {
3125 return;
3126 }
3127
3128 let SetExpr::Select(select) = query.body.as_mut() else {
3129 return;
3130 };
3131
3132 let outer_source_names = select_source_names_upper(select);
3133 let mut used_cte_names: HashSet<String> = query
3134 .with
3135 .as_ref()
3136 .map(|with| {
3137 with.cte_tables
3138 .iter()
3139 .map(|cte| cte.alias.name.value.to_ascii_uppercase())
3140 .collect()
3141 })
3142 .unwrap_or_default();
3143 used_cte_names.extend(outer_source_names.iter().cloned());
3144
3145 let mut new_ctes = Vec::new();
3146
3147 for table_with_joins in &mut select.from {
3148 if rule_filter.st005_forbid_subquery_in.forbid_from() {
3149 if let Some(cte) = rewrite_derived_table_factor_to_cte(
3150 &mut table_with_joins.relation,
3151 &outer_source_names,
3152 &mut used_cte_names,
3153 ) {
3154 new_ctes.push(cte);
3155 }
3156 }
3157
3158 if rule_filter.st005_forbid_subquery_in.forbid_join() {
3159 for join in &mut table_with_joins.joins {
3160 if let Some(cte) = rewrite_derived_table_factor_to_cte(
3161 &mut join.relation,
3162 &outer_source_names,
3163 &mut used_cte_names,
3164 ) {
3165 new_ctes.push(cte);
3166 }
3167 }
3168 }
3169 }
3170
3171 if new_ctes.is_empty() {
3172 return;
3173 }
3174
3175 let with = query.with.get_or_insert_with(|| With {
3176 with_token: AttachedToken::empty(),
3177 recursive: false,
3178 cte_tables: Vec::new(),
3179 });
3180 with.cte_tables.extend(new_ctes);
3181}
3182
3183fn rewrite_derived_table_factor_to_cte(
3184 relation: &mut TableFactor,
3185 outer_source_names: &HashSet<String>,
3186 used_cte_names: &mut HashSet<String>,
3187) -> Option<Cte> {
3188 let (lateral, subquery, alias) = match relation {
3189 TableFactor::Derived {
3190 lateral,
3191 subquery,
3192 alias,
3193 } => (lateral, subquery, alias),
3194 _ => return None,
3195 };
3196
3197 if *lateral {
3198 return None;
3199 }
3200
3201 if !matches!(subquery.body.as_ref(), SetExpr::Select(_))
3204 || query_text_references_outer_sources(subquery, outer_source_names)
3205 {
3206 return None;
3207 }
3208
3209 let cte_alias = alias.clone().unwrap_or_else(|| TableAlias {
3210 name: Ident::new(next_generated_cte_name(used_cte_names)),
3211 columns: Vec::new(),
3212 });
3213 let cte_name_ident = cte_alias.name.clone();
3214 let cte_name_upper = cte_name_ident.value.to_ascii_uppercase();
3215 used_cte_names.insert(cte_name_upper);
3216
3217 let cte = Cte {
3218 alias: cte_alias,
3219 query: subquery.clone(),
3220 from: None,
3221 materialized: None,
3222 closing_paren_token: AttachedToken::empty(),
3223 };
3224
3225 *relation = TableFactor::Table {
3226 name: vec![cte_name_ident].into(),
3227 alias: None,
3228 args: None,
3229 with_hints: Vec::new(),
3230 version: None,
3231 with_ordinality: false,
3232 partitions: Vec::new(),
3233 json_path: None,
3234 sample: None,
3235 index_hints: Vec::new(),
3236 };
3237
3238 Some(cte)
3239}
3240
3241fn next_generated_cte_name(used_cte_names: &HashSet<String>) -> String {
3242 let mut index = 1usize;
3243 loop {
3244 let candidate = format!("cte_subquery_{index}");
3245 if !used_cte_names.contains(&candidate.to_ascii_uppercase()) {
3246 return candidate;
3247 }
3248 index += 1;
3249 }
3250}
3251
3252fn query_text_references_outer_sources(
3253 query: &Query,
3254 outer_source_names: &HashSet<String>,
3255) -> bool {
3256 if outer_source_names.is_empty() {
3257 return false;
3258 }
3259
3260 let rendered_upper = query.to_string().to_ascii_uppercase();
3261 outer_source_names
3262 .iter()
3263 .any(|name| rendered_upper.contains(&format!("{name}.")))
3264}
3265
3266fn select_source_names_upper(select: &Select) -> HashSet<String> {
3267 let mut names = HashSet::new();
3268 for table in &select.from {
3269 collect_source_names_from_table_factor(&table.relation, &mut names);
3270 for join in &table.joins {
3271 collect_source_names_from_table_factor(&join.relation, &mut names);
3272 }
3273 }
3274 names
3275}
3276
3277fn collect_source_names_from_table_factor(relation: &TableFactor, names: &mut HashSet<String>) {
3278 match relation {
3279 TableFactor::Table { name, alias, .. } => {
3280 if let Some(last) = name.0.last().and_then(|part| part.as_ident()) {
3281 names.insert(last.value.to_ascii_uppercase());
3282 }
3283 if let Some(alias) = alias {
3284 names.insert(alias.name.value.to_ascii_uppercase());
3285 }
3286 }
3287 TableFactor::Derived { alias, .. }
3288 | TableFactor::TableFunction { alias, .. }
3289 | TableFactor::Function { alias, .. }
3290 | TableFactor::UNNEST { alias, .. }
3291 | TableFactor::JsonTable { alias, .. }
3292 | TableFactor::OpenJsonTable { alias, .. }
3293 | TableFactor::NestedJoin { alias, .. }
3294 | TableFactor::Pivot { alias, .. }
3295 | TableFactor::Unpivot { alias, .. } => {
3296 if let Some(alias) = alias {
3297 names.insert(alias.name.value.to_ascii_uppercase());
3298 }
3299 }
3300 _ => {}
3301 }
3302}
3303
3304fn rewrite_right_join_to_left(table_with_joins: &mut TableWithJoins) {
3305 while let Some(index) = table_with_joins
3306 .joins
3307 .iter()
3308 .position(|join| rewritable_right_join(&join.join_operator))
3309 {
3310 rewrite_right_join_at_index(table_with_joins, index);
3311 }
3312}
3313
3314fn rewrite_right_join_at_index(table_with_joins: &mut TableWithJoins, index: usize) {
3315 let mut suffix = table_with_joins.joins.split_off(index);
3316 let mut join = suffix.remove(0);
3317
3318 let old_operator = std::mem::replace(
3319 &mut join.join_operator,
3320 JoinOperator::CrossJoin(JoinConstraint::None),
3321 );
3322 let Some(new_operator) = rewritten_left_join_operator(old_operator) else {
3323 table_with_joins.joins.push(join);
3324 table_with_joins.joins.append(&mut suffix);
3325 return;
3326 };
3327
3328 let previous_relation = std::mem::replace(&mut table_with_joins.relation, join.relation);
3329 let prefix_joins = std::mem::take(&mut table_with_joins.joins);
3330
3331 join.relation = if prefix_joins.is_empty() {
3332 previous_relation
3333 } else {
3334 TableFactor::NestedJoin {
3335 table_with_joins: Box::new(TableWithJoins {
3336 relation: previous_relation,
3337 joins: prefix_joins,
3338 }),
3339 alias: None,
3340 }
3341 };
3342 join.join_operator = new_operator;
3343
3344 table_with_joins.joins.push(join);
3345 table_with_joins.joins.append(&mut suffix);
3346}
3347
3348fn rewritable_right_join(operator: &JoinOperator) -> bool {
3349 matches!(
3350 operator,
3351 JoinOperator::Right(_)
3352 | JoinOperator::RightOuter(_)
3353 | JoinOperator::RightSemi(_)
3354 | JoinOperator::RightAnti(_)
3355 )
3356}
3357
3358fn rewritten_left_join_operator(operator: JoinOperator) -> Option<JoinOperator> {
3359 match operator {
3360 JoinOperator::Right(constraint) => Some(JoinOperator::Left(constraint)),
3361 JoinOperator::RightOuter(constraint) => Some(JoinOperator::LeftOuter(constraint)),
3362 JoinOperator::RightSemi(constraint) => Some(JoinOperator::LeftSemi(constraint)),
3363 JoinOperator::RightAnti(constraint) => Some(JoinOperator::LeftAnti(constraint)),
3364 _ => None,
3365 }
3366}
3367
3368fn table_factor_alias_ident(relation: &TableFactor) -> Option<&Ident> {
3369 let alias = match relation {
3370 TableFactor::Table { alias, .. }
3371 | TableFactor::Derived { alias, .. }
3372 | TableFactor::TableFunction { alias, .. }
3373 | TableFactor::Function { alias, .. }
3374 | TableFactor::UNNEST { alias, .. }
3375 | TableFactor::JsonTable { alias, .. }
3376 | TableFactor::OpenJsonTable { alias, .. }
3377 | TableFactor::NestedJoin { alias, .. }
3378 | TableFactor::Pivot { alias, .. }
3379 | TableFactor::Unpivot { alias, .. } => alias.as_ref(),
3380 _ => None,
3381 }?;
3382
3383 Some(&alias.name)
3384}
3385
3386fn table_factor_reference_name(relation: &TableFactor) -> Option<String> {
3387 match relation {
3388 TableFactor::Table { name, alias, .. } => {
3389 if let Some(alias) = alias {
3390 Some(alias.name.value.clone())
3391 } else {
3392 name.0
3393 .last()
3394 .and_then(|part| part.as_ident())
3395 .map(|ident| ident.value.clone())
3396 }
3397 }
3398 _ => None,
3399 }
3400}
3401
3402fn rewrite_using_join_constraint(
3403 join_operator: &mut JoinOperator,
3404 left_ref: Option<&str>,
3405 right_ref: Option<&str>,
3406) {
3407 let (Some(left_ref), Some(right_ref)) = (left_ref, right_ref) else {
3408 return;
3409 };
3410
3411 let Some(constraint) = join_constraint_mut(join_operator) else {
3412 return;
3413 };
3414
3415 let JoinConstraint::Using(columns) = constraint else {
3416 return;
3417 };
3418
3419 if columns.is_empty() {
3420 return;
3421 }
3422
3423 let mut combined: Option<Expr> = None;
3424 for object_name in columns.iter() {
3425 let Some(column_ident) = object_name
3426 .0
3427 .last()
3428 .and_then(|part| part.as_ident())
3429 .cloned()
3430 else {
3431 continue;
3432 };
3433
3434 let equality = Expr::BinaryOp {
3435 left: Box::new(Expr::CompoundIdentifier(vec![
3436 Ident::new(left_ref),
3437 column_ident.clone(),
3438 ])),
3439 op: BinaryOperator::Eq,
3440 right: Box::new(Expr::CompoundIdentifier(vec![
3441 Ident::new(right_ref),
3442 column_ident,
3443 ])),
3444 };
3445
3446 combined = Some(match combined {
3447 Some(prev) => Expr::BinaryOp {
3448 left: Box::new(prev),
3449 op: BinaryOperator::And,
3450 right: Box::new(equality),
3451 },
3452 None => equality,
3453 });
3454 }
3455
3456 if let Some(on_expr) = combined {
3457 *constraint = JoinConstraint::On(on_expr);
3458 }
3459}
3460
3461fn fix_table_factor(relation: &mut TableFactor, rule_filter: &RuleFilter) {
3462 match relation {
3463 TableFactor::Table {
3464 args, with_hints, ..
3465 } => {
3466 if let Some(args) = args {
3467 for arg in &mut args.args {
3468 fix_function_arg(arg, rule_filter);
3469 }
3470 }
3471 for hint in with_hints {
3472 fix_expr(hint, rule_filter);
3473 }
3474 }
3475 TableFactor::Derived { subquery, .. } => fix_query(subquery, rule_filter),
3476 TableFactor::TableFunction { expr, .. } => fix_expr(expr, rule_filter),
3477 TableFactor::Function { args, .. } => {
3478 for arg in args {
3479 fix_function_arg(arg, rule_filter);
3480 }
3481 }
3482 TableFactor::UNNEST { array_exprs, .. } => {
3483 for expr in array_exprs {
3484 fix_expr(expr, rule_filter);
3485 }
3486 }
3487 TableFactor::NestedJoin {
3488 table_with_joins, ..
3489 } => {
3490 if rule_filter.allows(issue_codes::LINT_CV_008) {
3491 rewrite_right_join_to_left(table_with_joins);
3492 }
3493
3494 fix_table_factor(&mut table_with_joins.relation, rule_filter);
3495
3496 let mut left_ref = table_factor_reference_name(&table_with_joins.relation);
3497
3498 for join in &mut table_with_joins.joins {
3499 let right_ref = table_factor_reference_name(&join.relation);
3500 if rule_filter.allows(issue_codes::LINT_ST_007) {
3501 rewrite_using_join_constraint(
3502 &mut join.join_operator,
3503 left_ref.as_deref(),
3504 right_ref.as_deref(),
3505 );
3506 }
3507
3508 fix_table_factor(&mut join.relation, rule_filter);
3509 fix_join_operator(&mut join.join_operator, rule_filter);
3510
3511 if right_ref.is_some() {
3512 left_ref = right_ref;
3513 }
3514 }
3515 }
3516 TableFactor::Pivot {
3517 table,
3518 aggregate_functions,
3519 value_column,
3520 default_on_null,
3521 ..
3522 } => {
3523 fix_table_factor(table, rule_filter);
3524 for func in aggregate_functions {
3525 fix_expr(&mut func.expr, rule_filter);
3526 }
3527 for expr in value_column {
3528 fix_expr(expr, rule_filter);
3529 }
3530 if let Some(expr) = default_on_null {
3531 fix_expr(expr, rule_filter);
3532 }
3533 }
3534 TableFactor::Unpivot {
3535 table,
3536 value,
3537 columns,
3538 ..
3539 } => {
3540 fix_table_factor(table, rule_filter);
3541 fix_expr(value, rule_filter);
3542 for column in columns {
3543 fix_expr(&mut column.expr, rule_filter);
3544 }
3545 }
3546 TableFactor::JsonTable { json_expr, .. } => fix_expr(json_expr, rule_filter),
3547 TableFactor::OpenJsonTable { json_expr, .. } => fix_expr(json_expr, rule_filter),
3548 _ => {}
3549 }
3550}
3551
3552fn fix_join_operator(op: &mut JoinOperator, rule_filter: &RuleFilter) {
3553 match op {
3554 JoinOperator::Join(constraint)
3555 | JoinOperator::Inner(constraint)
3556 | JoinOperator::Left(constraint)
3557 | JoinOperator::LeftOuter(constraint)
3558 | JoinOperator::Right(constraint)
3559 | JoinOperator::RightOuter(constraint)
3560 | JoinOperator::FullOuter(constraint)
3561 | JoinOperator::CrossJoin(constraint)
3562 | JoinOperator::Semi(constraint)
3563 | JoinOperator::LeftSemi(constraint)
3564 | JoinOperator::RightSemi(constraint)
3565 | JoinOperator::Anti(constraint)
3566 | JoinOperator::LeftAnti(constraint)
3567 | JoinOperator::RightAnti(constraint)
3568 | JoinOperator::StraightJoin(constraint) => fix_join_constraint(constraint, rule_filter),
3569 JoinOperator::AsOf {
3570 match_condition,
3571 constraint,
3572 } => {
3573 fix_expr(match_condition, rule_filter);
3574 fix_join_constraint(constraint, rule_filter);
3575 }
3576 JoinOperator::CrossApply | JoinOperator::OuterApply => {}
3577 }
3578}
3579
3580fn join_constraint_mut(join_operator: &mut JoinOperator) -> Option<&mut JoinConstraint> {
3581 match join_operator {
3582 JoinOperator::Join(constraint)
3583 | JoinOperator::Inner(constraint)
3584 | JoinOperator::Left(constraint)
3585 | JoinOperator::LeftOuter(constraint)
3586 | JoinOperator::Right(constraint)
3587 | JoinOperator::RightOuter(constraint)
3588 | JoinOperator::FullOuter(constraint)
3589 | JoinOperator::CrossJoin(constraint)
3590 | JoinOperator::Semi(constraint)
3591 | JoinOperator::LeftSemi(constraint)
3592 | JoinOperator::RightSemi(constraint)
3593 | JoinOperator::Anti(constraint)
3594 | JoinOperator::LeftAnti(constraint)
3595 | JoinOperator::RightAnti(constraint)
3596 | JoinOperator::StraightJoin(constraint) => Some(constraint),
3597 JoinOperator::AsOf { constraint, .. } => Some(constraint),
3598 JoinOperator::CrossApply | JoinOperator::OuterApply => None,
3599 }
3600}
3601
3602fn fix_join_constraint(constraint: &mut JoinConstraint, rule_filter: &RuleFilter) {
3603 if let JoinConstraint::On(expr) = constraint {
3604 fix_expr(expr, rule_filter);
3605 }
3606}
3607
3608fn fix_order_by(order_by: &mut OrderBy, rule_filter: &RuleFilter) {
3609 if let OrderByKind::Expressions(exprs) = &mut order_by.kind {
3610 for order_expr in exprs.iter_mut() {
3611 fix_expr(&mut order_expr.expr, rule_filter);
3612 }
3613 }
3614
3615 if let Some(interpolate) = order_by.interpolate.as_mut() {
3616 if let Some(exprs) = interpolate.exprs.as_mut() {
3617 for expr in exprs {
3618 if let Some(inner) = expr.expr.as_mut() {
3619 fix_expr(inner, rule_filter);
3620 }
3621 }
3622 }
3623 }
3624}
3625
3626fn fix_limit_clause(limit_clause: &mut LimitClause, rule_filter: &RuleFilter) {
3627 match limit_clause {
3628 LimitClause::LimitOffset {
3629 limit,
3630 offset,
3631 limit_by,
3632 } => {
3633 if let Some(limit) = limit {
3634 fix_expr(limit, rule_filter);
3635 }
3636 if let Some(offset) = offset {
3637 fix_expr(&mut offset.value, rule_filter);
3638 }
3639 for expr in limit_by {
3640 fix_expr(expr, rule_filter);
3641 }
3642 }
3643 LimitClause::OffsetCommaLimit { offset, limit } => {
3644 fix_expr(offset, rule_filter);
3645 fix_expr(limit, rule_filter);
3646 }
3647 }
3648}
3649
3650fn fix_expr(expr: &mut Expr, rule_filter: &RuleFilter) {
3651 match expr {
3652 Expr::BinaryOp { left, right, .. } => {
3653 fix_expr(left, rule_filter);
3654 fix_expr(right, rule_filter);
3655 }
3656 Expr::UnaryOp { expr: inner, .. }
3657 | Expr::Nested(inner)
3658 | Expr::IsNull(inner)
3659 | Expr::IsNotNull(inner)
3660 | Expr::IsTrue(inner)
3661 | Expr::IsNotTrue(inner)
3662 | Expr::IsFalse(inner)
3663 | Expr::IsNotFalse(inner)
3664 | Expr::IsUnknown(inner)
3665 | Expr::IsNotUnknown(inner) => fix_expr(inner, rule_filter),
3666 Expr::Case {
3667 operand,
3668 conditions,
3669 else_result,
3670 ..
3671 } => {
3672 if let Some(operand) = operand.as_mut() {
3673 fix_expr(operand, rule_filter);
3674 }
3675 for case_when in conditions {
3676 fix_expr(&mut case_when.condition, rule_filter);
3677 fix_expr(&mut case_when.result, rule_filter);
3678 }
3679 if let Some(else_result) = else_result.as_mut() {
3680 fix_expr(else_result, rule_filter);
3681 }
3682 }
3683 Expr::Function(func) => fix_function(func, rule_filter),
3684 Expr::Cast { expr: inner, .. } => fix_expr(inner, rule_filter),
3685 Expr::InSubquery {
3686 expr: inner,
3687 subquery,
3688 ..
3689 } => {
3690 fix_expr(inner, rule_filter);
3691 fix_query(subquery, rule_filter);
3692 }
3693 Expr::Subquery(subquery) | Expr::Exists { subquery, .. } => {
3694 fix_query(subquery, rule_filter)
3695 }
3696 Expr::Between {
3697 expr: target,
3698 low,
3699 high,
3700 ..
3701 } => {
3702 fix_expr(target, rule_filter);
3703 fix_expr(low, rule_filter);
3704 fix_expr(high, rule_filter);
3705 }
3706 Expr::InList {
3707 expr: target, list, ..
3708 } => {
3709 fix_expr(target, rule_filter);
3710 for item in list {
3711 fix_expr(item, rule_filter);
3712 }
3713 }
3714 Expr::Tuple(items) => {
3715 for item in items {
3716 fix_expr(item, rule_filter);
3717 }
3718 }
3719 _ => {}
3720 }
3721
3722 if rule_filter.allows(issue_codes::LINT_ST_004) {
3727 if let Some(rewritten) = nested_case_rewrite(expr) {
3728 *expr = rewritten;
3729 }
3730 }
3731}
3732
3733fn fix_function(func: &mut Function, rule_filter: &RuleFilter) {
3734 if let FunctionArguments::List(arg_list) = &mut func.args {
3735 for arg in &mut arg_list.args {
3736 fix_function_arg(arg, rule_filter);
3737 }
3738 for clause in &mut arg_list.clauses {
3739 match clause {
3740 FunctionArgumentClause::OrderBy(order_by_exprs) => {
3741 for order_by_expr in order_by_exprs {
3742 fix_expr(&mut order_by_expr.expr, rule_filter);
3743 }
3744 }
3745 FunctionArgumentClause::Limit(expr) => fix_expr(expr, rule_filter),
3746 _ => {}
3747 }
3748 }
3749 }
3750
3751 if let Some(filter) = func.filter.as_mut() {
3752 fix_expr(filter, rule_filter);
3753 }
3754
3755 for order_expr in &mut func.within_group {
3756 fix_expr(&mut order_expr.expr, rule_filter);
3757 }
3758}
3759
3760fn fix_function_arg(arg: &mut FunctionArg, rule_filter: &RuleFilter) {
3761 match arg {
3762 FunctionArg::Named { arg, .. }
3763 | FunctionArg::ExprNamed { arg, .. }
3764 | FunctionArg::Unnamed(arg) => {
3765 if let FunctionArgExpr::Expr(expr) = arg {
3766 fix_expr(expr, rule_filter);
3767 }
3768 }
3769 }
3770}
3771
3772fn nested_case_rewrite(expr: &Expr) -> Option<Expr> {
3773 let Expr::Case {
3774 case_token,
3775 operand: outer_operand,
3776 conditions: outer_conditions,
3777 else_result: Some(outer_else),
3778 end_token,
3779 } = expr
3780 else {
3781 return None;
3782 };
3783
3784 if outer_conditions.is_empty() {
3785 return None;
3786 }
3787
3788 let Expr::Case {
3789 operand: inner_operand,
3790 conditions: inner_conditions,
3791 else_result: inner_else,
3792 ..
3793 } = nested_case_expr(outer_else.as_ref())?
3794 else {
3795 return None;
3796 };
3797
3798 if inner_conditions.is_empty() {
3799 return None;
3800 }
3801
3802 if !case_operands_match(outer_operand.as_deref(), inner_operand.as_deref()) {
3803 return None;
3804 }
3805
3806 let mut merged_conditions = outer_conditions.clone();
3807 merged_conditions.extend(inner_conditions.iter().cloned());
3808
3809 Some(Expr::Case {
3810 case_token: case_token.clone(),
3811 operand: outer_operand.clone(),
3812 conditions: merged_conditions,
3813 else_result: inner_else.clone(),
3814 end_token: end_token.clone(),
3815 })
3816}
3817
3818fn nested_case_expr(expr: &Expr) -> Option<&Expr> {
3819 match expr {
3820 Expr::Case { .. } => Some(expr),
3821 Expr::Nested(inner) => nested_case_expr(inner),
3822 _ => None,
3823 }
3824}
3825
3826fn case_operands_match(outer: Option<&Expr>, inner: Option<&Expr>) -> bool {
3827 match (outer, inner) {
3828 (None, None) => true,
3829 (Some(left), Some(right)) => format!("{left}") == format!("{right}"),
3830 _ => false,
3831 }
3832}
3833
3834#[cfg(test)]
3835mod tests {
3836 use super::*;
3837 use flowscope_core::{
3838 analyze, issue_codes, AnalysisOptions, AnalyzeRequest, Dialect, LintConfig,
3839 };
3840
3841 fn default_lint_config() -> LintConfig {
3842 LintConfig {
3843 enabled: true,
3844 disabled_rules: vec![],
3845 rule_configs: std::collections::BTreeMap::new(),
3846 }
3847 }
3848
3849 fn lint_config_keep_only_rule(rule_code: &str, mut config: LintConfig) -> LintConfig {
3850 let disabled_rules = flowscope_core::linter::rules::all_rules(&default_lint_config())
3851 .into_iter()
3852 .map(|rule| rule.code().to_string())
3853 .filter(|code| !code.eq_ignore_ascii_case(rule_code))
3854 .collect();
3855 config.disabled_rules = disabled_rules;
3856 config
3857 }
3858
3859 fn lint_rule_count_with_config(sql: &str, code: &str, lint_config: &LintConfig) -> usize {
3860 let request = AnalyzeRequest {
3861 sql: sql.to_string(),
3862 files: None,
3863 dialect: Dialect::Generic,
3864 source_name: None,
3865 options: Some(AnalysisOptions {
3866 lint: Some(lint_config.clone()),
3867 ..Default::default()
3868 }),
3869 schema: None,
3870 #[cfg(feature = "templating")]
3871 template_config: None,
3872 };
3873
3874 analyze(&request)
3875 .issues
3876 .iter()
3877 .filter(|issue| issue.code == code)
3878 .count()
3879 }
3880
3881 fn lint_rule_count_with_config_in_dialect(
3882 sql: &str,
3883 code: &str,
3884 dialect: Dialect,
3885 lint_config: &LintConfig,
3886 ) -> usize {
3887 let request = AnalyzeRequest {
3888 sql: sql.to_string(),
3889 files: None,
3890 dialect,
3891 source_name: None,
3892 options: Some(AnalysisOptions {
3893 lint: Some(lint_config.clone()),
3894 ..Default::default()
3895 }),
3896 schema: None,
3897 #[cfg(feature = "templating")]
3898 template_config: None,
3899 };
3900
3901 analyze(&request)
3902 .issues
3903 .iter()
3904 .filter(|issue| issue.code == code)
3905 .count()
3906 }
3907
3908 fn lint_rule_count(sql: &str, code: &str) -> usize {
3909 lint_rule_count_with_config(sql, code, &default_lint_config())
3910 }
3911
3912 fn apply_fix_with_config(sql: &str, lint_config: &LintConfig) -> FixOutcome {
3913 apply_lint_fixes_with_lint_config(sql, Dialect::Generic, lint_config).expect("fix result")
3914 }
3915
3916 fn apply_core_only_fixes(sql: &str) -> FixOutcome {
3917 apply_lint_fixes_with_options(
3918 sql,
3919 Dialect::Generic,
3920 &default_lint_config(),
3921 FixOptions {
3922 include_unsafe_fixes: true,
3923 include_rewrite_candidates: false,
3924 },
3925 )
3926 .expect("fix result")
3927 }
3928
3929 fn sample_outcome(skipped_counts: FixSkippedCounts) -> FixOutcome {
3930 FixOutcome {
3931 sql: String::new(),
3932 counts: FixCounts::default(),
3933 changed: false,
3934 skipped_due_to_comments: false,
3935 skipped_due_to_regression: false,
3936 skipped_counts,
3937 }
3938 }
3939
3940 #[test]
3941 fn collect_fix_candidate_stats_always_counts_display_only_as_blocked() {
3942 let outcome = sample_outcome(FixSkippedCounts {
3943 unsafe_skipped: 1,
3944 protected_range_blocked: 2,
3945 overlap_conflict_blocked: 3,
3946 display_only: 4,
3947 });
3948
3949 let stats = collect_fix_candidate_stats(
3950 &outcome,
3951 LintFixRuntimeOptions {
3952 include_unsafe_fixes: false,
3953 legacy_ast_fixes: false,
3954 },
3955 );
3956
3957 assert_eq!(stats.skipped, 0);
3958 assert_eq!(stats.blocked, 10);
3959 assert_eq!(stats.blocked_unsafe, 1);
3960 assert_eq!(stats.blocked_display_only, 4);
3961 assert_eq!(stats.blocked_protected_range, 2);
3962 assert_eq!(stats.blocked_overlap_conflict, 3);
3963 }
3964
3965 #[test]
3966 fn collect_fix_candidate_stats_excludes_unsafe_when_unsafe_fixes_enabled() {
3967 let outcome = sample_outcome(FixSkippedCounts {
3968 unsafe_skipped: 2,
3969 protected_range_blocked: 1,
3970 overlap_conflict_blocked: 1,
3971 display_only: 3,
3972 });
3973
3974 let stats = collect_fix_candidate_stats(
3975 &outcome,
3976 LintFixRuntimeOptions {
3977 include_unsafe_fixes: true,
3978 legacy_ast_fixes: false,
3979 },
3980 );
3981
3982 assert_eq!(stats.blocked, 5);
3983 assert_eq!(stats.blocked_unsafe, 0);
3984 assert_eq!(stats.blocked_display_only, 3);
3985 }
3986
3987 #[test]
3988 fn mostly_unfixable_residual_detects_dominated_known_residuals() {
3989 let counts = std::collections::BTreeMap::from([
3990 (issue_codes::LINT_LT_005.to_string(), 140usize),
3991 (issue_codes::LINT_RF_002.to_string(), 116usize),
3992 (issue_codes::LINT_AL_003.to_string(), 43usize),
3993 (issue_codes::LINT_RF_004.to_string(), 2usize),
3994 (issue_codes::LINT_ST_009.to_string(), 1usize),
3995 ]);
3996 assert!(is_mostly_unfixable_residual(&counts));
3997 }
3998
3999 #[test]
4000 fn mostly_unfixable_residual_rejects_when_fixable_tail_is_material() {
4001 let counts = std::collections::BTreeMap::from([
4002 (issue_codes::LINT_LT_005.to_string(), 20usize),
4003 (issue_codes::LINT_RF_002.to_string(), 10usize),
4004 (issue_codes::LINT_ST_009.to_string(), 8usize),
4005 (issue_codes::LINT_LT_003.to_string(), 3usize),
4006 ]);
4007 assert!(!is_mostly_unfixable_residual(&counts));
4008 }
4009
4010 #[test]
4011 fn am005_outer_mode_full_join_fix_output() {
4012 let lint_config = LintConfig {
4013 enabled: true,
4014 disabled_rules: vec![issue_codes::LINT_CV_008.to_string()],
4015 rule_configs: std::collections::BTreeMap::from([(
4016 "ambiguous.join".to_string(),
4017 serde_json::json!({"fully_qualify_join_types": "outer"}),
4018 )]),
4019 };
4020 let sql = "SELECT a FROM t FULL JOIN u ON t.id = u.id";
4021 assert_eq!(
4022 lint_rule_count_with_config(
4023 "SELECT a FROM t FULL OUTER JOIN u ON t.id = u.id",
4024 issue_codes::LINT_AM_005,
4025 &lint_config,
4026 ),
4027 0
4028 );
4029 let out = apply_fix_with_config(sql, &lint_config);
4030 assert!(
4031 out.sql.to_ascii_uppercase().contains("FULL OUTER JOIN"),
4032 "expected FULL OUTER JOIN in fixed SQL, got: {}",
4033 out.sql
4034 );
4035 assert_eq!(fix_count_for_code(&out.counts, issue_codes::LINT_AM_005), 1);
4036 }
4037
4038 fn fix_count_for_code(counts: &FixCounts, code: &str) -> usize {
4039 counts.get(code)
4040 }
4041
4042 #[test]
4043 fn lint_rule_counts_includes_parse_errors() {
4044 let counts = lint_rule_counts("SELECT (", Dialect::Generic, &default_lint_config());
4045 assert!(
4046 counts.get(issue_codes::PARSE_ERROR).copied().unwrap_or(0) > 0,
4047 "invalid SQL should contribute PARSE_ERROR to regression counts"
4048 );
4049 }
4050
4051 #[test]
4052 fn parse_error_regression_is_detected_even_with_lint_improvements() {
4053 let before = std::collections::BTreeMap::from([(issue_codes::LINT_ST_005.to_string(), 1)]);
4054 let after = std::collections::BTreeMap::from([(issue_codes::PARSE_ERROR.to_string(), 1)]);
4055 let removed = FixCounts::from_removed(&before, &after);
4056
4057 assert_eq!(
4058 removed.total(),
4059 1,
4060 "lint-only comparison can still look improved"
4061 );
4062 assert!(
4063 parse_errors_increased(&before, &after),
4064 "introduced parse errors must force regression"
4065 );
4066 }
4067
4068 #[test]
4069 fn lint_improvements_can_mask_total_violation_regressions() {
4070 let before = std::collections::BTreeMap::from([
4071 (issue_codes::LINT_LT_002.to_string(), 2usize),
4072 (issue_codes::LINT_LT_001.to_string(), 0usize),
4073 ]);
4074 let after = std::collections::BTreeMap::from([
4075 (issue_codes::LINT_LT_002.to_string(), 1usize),
4076 (issue_codes::LINT_LT_001.to_string(), 2usize),
4077 ]);
4078 let removed = FixCounts::from_removed(&before, &after);
4079 let before_total: usize = before.values().sum();
4080 let after_total: usize = after.values().sum();
4081
4082 assert_eq!(
4083 removed.total(),
4084 1,
4085 "a rule-level improvement can still be observed"
4086 );
4087 assert!(
4088 after_total > before_total,
4089 "strict regression guard must reject net-violation increases"
4090 );
4091 }
4092
4093 #[test]
4094 fn lt03_improvement_allows_lt05_tradeoff_at_equal_totals() {
4095 let before = std::collections::BTreeMap::from([
4096 (issue_codes::LINT_LT_003.to_string(), 1usize),
4097 (issue_codes::LINT_LT_005.to_string(), 5usize),
4098 ]);
4099 let after = std::collections::BTreeMap::from([
4100 (issue_codes::LINT_LT_003.to_string(), 0usize),
4101 (issue_codes::LINT_LT_005.to_string(), 6usize),
4102 ]);
4103 let core_rules = std::collections::HashSet::from([
4104 issue_codes::LINT_LT_003.to_string(),
4105 issue_codes::LINT_LT_005.to_string(),
4106 ]);
4107
4108 assert!(
4109 !core_autofix_rules_not_improved(&before, &after, &core_rules),
4110 "LT03 improvements should be allowed to trade against LT05 at equal totals"
4111 );
4112 }
4113
4114 #[test]
4115 fn lt05_tradeoff_is_not_allowed_without_lt03_improvement() {
4116 let before = std::collections::BTreeMap::from([
4117 (issue_codes::LINT_LT_003.to_string(), 1usize),
4118 (issue_codes::LINT_LT_005.to_string(), 5usize),
4119 ]);
4120 let after = std::collections::BTreeMap::from([
4121 (issue_codes::LINT_LT_003.to_string(), 1usize),
4122 (issue_codes::LINT_LT_005.to_string(), 6usize),
4123 ]);
4124 let core_rules = std::collections::HashSet::from([
4125 issue_codes::LINT_LT_003.to_string(),
4126 issue_codes::LINT_LT_005.to_string(),
4127 ]);
4128
4129 assert!(
4130 core_autofix_rules_not_improved(&before, &after, &core_rules),
4131 "without LT03 improvement, LT05 worsening remains blocked"
4132 );
4133 }
4134
4135 fn assert_rule_case(
4136 sql: &str,
4137 code: &str,
4138 expected_before: usize,
4139 expected_after: usize,
4140 expected_fix_count: usize,
4141 ) {
4142 let before = lint_rule_count(sql, code);
4143 assert_eq!(
4144 before, expected_before,
4145 "unexpected initial lint count for {code} in SQL: {sql}"
4146 );
4147
4148 let out = apply_core_only_fixes(sql);
4149 assert!(
4150 !out.skipped_due_to_comments,
4151 "test SQL should not be skipped"
4152 );
4153 assert_eq!(
4154 fix_count_for_code(&out.counts, code),
4155 expected_fix_count,
4156 "unexpected fix count for {code} in SQL: {sql}"
4157 );
4158
4159 if expected_fix_count > 0 {
4160 assert!(out.changed, "expected SQL to change for {code}: {sql}");
4161 }
4162
4163 let after = lint_rule_count(&out.sql, code);
4164 assert_eq!(
4165 after, expected_after,
4166 "unexpected lint count after fix for {code}. SQL: {}",
4167 out.sql
4168 );
4169
4170 let second_pass = apply_core_only_fixes(&out.sql);
4171 assert_eq!(
4172 fix_count_for_code(&second_pass.counts, code),
4173 0,
4174 "expected idempotent second pass for {code}"
4175 );
4176 }
4177
4178 fn assert_rule_case_with_config(
4179 sql: &str,
4180 code: &str,
4181 expected_before: usize,
4182 expected_after: usize,
4183 expected_fix_count: usize,
4184 lint_config: &LintConfig,
4185 ) {
4186 let before = lint_rule_count_with_config(sql, code, lint_config);
4187 assert_eq!(
4188 before, expected_before,
4189 "unexpected initial lint count for {code} in SQL: {sql}"
4190 );
4191
4192 let out = apply_fix_with_config(sql, lint_config);
4193 assert!(
4194 !out.skipped_due_to_comments,
4195 "test SQL should not be skipped"
4196 );
4197 assert_eq!(
4198 fix_count_for_code(&out.counts, code),
4199 expected_fix_count,
4200 "unexpected fix count for {code} in SQL: {sql}"
4201 );
4202
4203 if expected_fix_count > 0 {
4204 assert!(out.changed, "expected SQL to change for {code}: {sql}");
4205 }
4206
4207 let after = lint_rule_count_with_config(&out.sql, code, lint_config);
4208 assert_eq!(
4209 after, expected_after,
4210 "unexpected lint count after fix for {code}. SQL: {}",
4211 out.sql
4212 );
4213
4214 let second_pass = apply_fix_with_config(&out.sql, lint_config);
4215 assert_eq!(
4216 fix_count_for_code(&second_pass.counts, code),
4217 0,
4218 "expected idempotent second pass for {code}"
4219 );
4220 }
4221
4222 #[test]
4223 fn sqlfluff_am003_cases_are_fixed() {
4224 let cases = [
4225 ("SELECT DISTINCT col FROM t GROUP BY col", 1, 0, 1),
4226 (
4227 "SELECT * FROM (SELECT DISTINCT a FROM t GROUP BY a) AS sub",
4228 1,
4229 0,
4230 1,
4231 ),
4232 (
4233 "WITH cte AS (SELECT DISTINCT a FROM t GROUP BY a) SELECT * FROM cte",
4234 1,
4235 0,
4236 1,
4237 ),
4238 (
4239 "CREATE VIEW v AS SELECT DISTINCT a FROM t GROUP BY a",
4240 1,
4241 0,
4242 1,
4243 ),
4244 (
4245 "INSERT INTO target SELECT DISTINCT a FROM t GROUP BY a",
4246 1,
4247 0,
4248 1,
4249 ),
4250 (
4251 "SELECT a FROM t UNION ALL SELECT DISTINCT b FROM t2 GROUP BY b",
4252 1,
4253 0,
4254 1,
4255 ),
4256 ("SELECT a, b FROM t", 0, 0, 0),
4257 ];
4258
4259 for (sql, before, after, fix_count) in cases {
4260 assert_rule_case(sql, issue_codes::LINT_AM_001, before, after, fix_count);
4261 }
4262 }
4263
4264 #[test]
4265 fn sqlfluff_am001_cases_are_fixed_or_unchanged() {
4266 let lint_config = LintConfig {
4267 enabled: true,
4268 disabled_rules: vec![issue_codes::LINT_LT_011.to_string()],
4269 rule_configs: std::collections::BTreeMap::new(),
4270 };
4271 let cases = [
4272 (
4273 "SELECT a, b FROM tbl UNION SELECT c, d FROM tbl1",
4274 1,
4275 0,
4276 1,
4277 Some("DISTINCT SELECT"),
4278 ),
4279 (
4280 "SELECT a, b FROM tbl UNION ALL SELECT c, d FROM tbl1",
4281 0,
4282 0,
4283 0,
4284 None,
4285 ),
4286 (
4287 "SELECT a, b FROM tbl UNION DISTINCT SELECT c, d FROM tbl1",
4288 0,
4289 0,
4290 0,
4291 None,
4292 ),
4293 (
4294 "select a, b from tbl union select c, d from tbl1",
4295 1,
4296 0,
4297 1,
4298 Some("DISTINCT SELECT"),
4299 ),
4300 ];
4301
4302 for (sql, before, after, fix_count, expected_text) in cases {
4303 assert_rule_case_with_config(
4304 sql,
4305 issue_codes::LINT_AM_002,
4306 before,
4307 after,
4308 fix_count,
4309 &lint_config,
4310 );
4311
4312 if let Some(expected) = expected_text {
4313 let out = apply_fix_with_config(sql, &lint_config);
4314 assert!(
4315 out.sql.to_ascii_uppercase().contains(expected),
4316 "expected {expected:?} in fixed SQL, got: {}",
4317 out.sql
4318 );
4319 }
4320 }
4321 }
4322
4323 #[test]
4324 fn sqlfluff_am005_cases_are_fixed_or_unchanged() {
4325 let cases = [
4326 (
4327 "SELECT * FROM t ORDER BY a, b DESC",
4328 1,
4329 0,
4330 1,
4331 Some("ORDER BY A ASC, B DESC"),
4332 ),
4333 (
4334 "SELECT * FROM t ORDER BY a DESC, b",
4335 1,
4336 0,
4337 1,
4338 Some("ORDER BY A DESC, B ASC"),
4339 ),
4340 (
4341 "SELECT * FROM t ORDER BY a DESC, b NULLS LAST",
4342 1,
4343 0,
4344 1,
4345 Some("ORDER BY A DESC, B ASC NULLS LAST"),
4346 ),
4347 ("SELECT * FROM t ORDER BY a, b", 0, 0, 0, None),
4348 ("SELECT * FROM t ORDER BY a ASC, b DESC", 0, 0, 0, None),
4349 ];
4350
4351 for (sql, before, after, fix_count, expected_text) in cases {
4352 assert_rule_case(sql, issue_codes::LINT_AM_003, before, after, fix_count);
4353
4354 if let Some(expected) = expected_text {
4355 let out = apply_lint_fixes(sql, Dialect::Generic, &[]).expect("fix result");
4356 assert!(
4357 out.sql.to_ascii_uppercase().contains(expected),
4358 "expected {expected:?} in fixed SQL, got: {}",
4359 out.sql
4360 );
4361 }
4362 }
4363 }
4364
4365 #[test]
4366 fn sqlfluff_am006_cases_are_fixed_or_unchanged() {
4367 let cases = [
4368 (
4369 "SELECT a FROM t JOIN u ON t.id = u.id",
4370 1,
4371 0,
4372 1,
4373 Some("INNER JOIN"),
4374 ),
4375 (
4376 "SELECT a FROM t JOIN u ON t.id = u.id JOIN v ON u.id = v.id",
4377 2,
4378 0,
4379 2,
4380 Some("INNER JOIN U"),
4381 ),
4382 ("SELECT a FROM t INNER JOIN u ON t.id = u.id", 0, 0, 0, None),
4383 ("SELECT a FROM t LEFT JOIN u ON t.id = u.id", 0, 0, 0, None),
4384 ];
4385
4386 for (sql, before, after, fix_count, expected_text) in cases {
4387 assert_rule_case(sql, issue_codes::LINT_AM_005, before, after, fix_count);
4388
4389 if let Some(expected) = expected_text {
4390 let out = apply_lint_fixes(sql, Dialect::Generic, &[]).expect("fix result");
4391 assert!(
4392 out.sql.to_ascii_uppercase().contains(expected),
4393 "expected {expected:?} in fixed SQL, got: {}",
4394 out.sql
4395 );
4396 }
4397 }
4398 }
4399
4400 #[test]
4401 fn sqlfluff_am005_outer_and_both_configs_are_fixed() {
4402 let outer_config = LintConfig {
4403 enabled: true,
4404 disabled_rules: vec![issue_codes::LINT_CV_008.to_string()],
4405 rule_configs: std::collections::BTreeMap::from([(
4406 "ambiguous.join".to_string(),
4407 serde_json::json!({"fully_qualify_join_types": "outer"}),
4408 )]),
4409 };
4410 let both_config = LintConfig {
4411 enabled: true,
4412 disabled_rules: vec![issue_codes::LINT_CV_008.to_string()],
4413 rule_configs: std::collections::BTreeMap::from([(
4414 "ambiguous.join".to_string(),
4415 serde_json::json!({"fully_qualify_join_types": "both"}),
4416 )]),
4417 };
4418
4419 let outer_cases = [
4420 (
4421 "SELECT a FROM t LEFT JOIN u ON t.id = u.id",
4422 1,
4423 0,
4424 1,
4425 Some("LEFT OUTER JOIN"),
4426 ),
4427 (
4428 "SELECT a FROM t RIGHT JOIN u ON t.id = u.id",
4429 1,
4430 0,
4431 1,
4432 Some("RIGHT OUTER JOIN"),
4433 ),
4434 (
4435 "SELECT a FROM t FULL JOIN u ON t.id = u.id",
4436 1,
4437 0,
4438 1,
4439 Some("FULL OUTER JOIN"),
4440 ),
4441 (
4442 "SELECT a FROM t full join u ON t.id = u.id",
4443 1,
4444 0,
4445 1,
4446 Some("FULL OUTER JOIN"),
4447 ),
4448 ("SELECT a FROM t JOIN u ON t.id = u.id", 0, 0, 0, None),
4449 ];
4450 for (sql, before, after, fix_count, expected_text) in outer_cases {
4451 assert_rule_case_with_config(
4452 sql,
4453 issue_codes::LINT_AM_005,
4454 before,
4455 after,
4456 fix_count,
4457 &outer_config,
4458 );
4459 if let Some(expected) = expected_text {
4460 let out = apply_fix_with_config(sql, &outer_config);
4461 assert!(
4462 out.sql.to_ascii_uppercase().contains(expected),
4463 "expected {expected:?} in fixed SQL, got: {}",
4464 out.sql
4465 );
4466 }
4467 }
4468
4469 let both_cases = [
4470 (
4471 "SELECT a FROM t JOIN u ON t.id = u.id",
4472 1,
4473 0,
4474 1,
4475 Some("INNER JOIN"),
4476 ),
4477 (
4478 "SELECT a FROM t LEFT JOIN u ON t.id = u.id",
4479 1,
4480 0,
4481 1,
4482 Some("LEFT OUTER JOIN"),
4483 ),
4484 (
4485 "SELECT a FROM t FULL JOIN u ON t.id = u.id",
4486 1,
4487 0,
4488 1,
4489 Some("FULL OUTER JOIN"),
4490 ),
4491 ];
4492 for (sql, before, after, fix_count, expected_text) in both_cases {
4493 assert_rule_case_with_config(
4494 sql,
4495 issue_codes::LINT_AM_005,
4496 before,
4497 after,
4498 fix_count,
4499 &both_config,
4500 );
4501 if let Some(expected) = expected_text {
4502 let out = apply_fix_with_config(sql, &both_config);
4503 assert!(
4504 out.sql.to_ascii_uppercase().contains(expected),
4505 "expected {expected:?} in fixed SQL, got: {}",
4506 out.sql
4507 );
4508 }
4509 }
4510 }
4511
4512 #[test]
4513 fn sqlfluff_am009_cases_are_fixed_or_unchanged() {
4514 let cases = [
4515 (
4516 "SELECT foo.a, bar.b FROM foo INNER JOIN bar",
4517 1,
4518 0,
4519 1,
4520 Some("CROSS JOIN BAR"),
4521 ),
4522 (
4523 "SELECT foo.a, bar.b FROM foo LEFT JOIN bar",
4524 1,
4525 0,
4526 1,
4527 Some("CROSS JOIN BAR"),
4528 ),
4529 (
4530 "SELECT foo.a, bar.b FROM foo JOIN bar WHERE foo.a = bar.a OR foo.x = 3",
4531 0,
4532 0,
4533 0,
4534 None,
4535 ),
4536 ("SELECT foo.a, bar.b FROM foo CROSS JOIN bar", 0, 0, 0, None),
4537 (
4538 "SELECT foo.id, bar.id FROM foo LEFT JOIN bar USING (id)",
4539 0,
4540 0,
4541 0,
4542 None,
4543 ),
4544 ];
4545
4546 for (sql, before, after, fix_count, expected_text) in cases {
4547 assert_rule_case(sql, issue_codes::LINT_AM_008, before, after, fix_count);
4548
4549 if let Some(expected) = expected_text {
4550 let out = apply_lint_fixes(sql, Dialect::Generic, &[]).expect("fix result");
4551 assert!(
4552 out.sql.to_ascii_uppercase().contains(expected),
4553 "expected {expected:?} in fixed SQL, got: {}",
4554 out.sql
4555 );
4556 }
4557 }
4558 }
4559
4560 #[test]
4561 fn sqlfluff_al007_force_enabled_single_table_alias_is_fixed() {
4562 let lint_config = LintConfig {
4563 enabled: true,
4564 disabled_rules: vec![],
4565 rule_configs: std::collections::BTreeMap::from([(
4566 "aliasing.forbid".to_string(),
4567 serde_json::json!({"force_enable": true}),
4568 )]),
4569 };
4570 let sql = "SELECT u.id FROM users u";
4571 assert_rule_case_with_config(sql, issue_codes::LINT_AL_007, 1, 0, 1, &lint_config);
4572
4573 let out = apply_fix_with_config(sql, &lint_config);
4574 let fixed_upper = out.sql.to_ascii_uppercase();
4575 assert!(
4576 fixed_upper.contains("FROM USERS"),
4577 "expected table alias to be removed: {}",
4578 out.sql
4579 );
4580 assert!(
4581 !fixed_upper.contains("FROM USERS U"),
4582 "expected unnecessary table alias to be removed: {}",
4583 out.sql
4584 );
4585 assert!(
4586 fixed_upper.contains("USERS.ID"),
4587 "expected references to use table name after alias removal: {}",
4588 out.sql
4589 );
4590 }
4591
4592 #[test]
4593 fn sqlfluff_al009_fix_respects_case_sensitive_mode() {
4594 let lint_config = LintConfig {
4595 enabled: true,
4596 disabled_rules: vec![issue_codes::LINT_CP_002.to_string()],
4599 rule_configs: std::collections::BTreeMap::from([(
4600 "aliasing.self_alias.column".to_string(),
4601 serde_json::json!({"alias_case_check": "case_sensitive"}),
4602 )]),
4603 };
4604 let sql = "SELECT a AS A FROM t";
4605 assert_rule_case_with_config(sql, issue_codes::LINT_AL_009, 0, 0, 0, &lint_config);
4606
4607 let out = apply_fix_with_config(sql, &lint_config);
4608 assert!(
4609 out.sql.contains("AS A"),
4610 "case-sensitive mode should keep case-mismatched alias: {}",
4611 out.sql
4612 );
4613 }
4614
4615 #[test]
4616 fn sqlfluff_al009_ast_fix_keeps_table_aliases() {
4617 let lint_config = LintConfig {
4618 enabled: true,
4619 disabled_rules: vec![issue_codes::LINT_AL_007.to_string()],
4620 rule_configs: std::collections::BTreeMap::new(),
4621 };
4622 let sql = "SELECT t.a AS a FROM t AS t";
4623 assert_rule_case_with_config(sql, issue_codes::LINT_AL_009, 1, 0, 1, &lint_config);
4624
4625 let out = apply_fix_with_config(sql, &lint_config);
4626 let fixed_upper = out.sql.to_ascii_uppercase();
4627 assert!(
4628 fixed_upper.contains("FROM T AS T"),
4629 "AL09 fix should not remove table alias declarations: {}",
4630 out.sql
4631 );
4632 assert!(
4633 !fixed_upper.contains("T.A AS A"),
4634 "expected only column self-alias to be removed: {}",
4635 out.sql
4636 );
4637 }
4638
4639 #[test]
4640 fn sqlfluff_st002_unnecessary_case_fix_cases() {
4641 let cases = [
4642 (
4644 "SELECT CASE WHEN x > 0 THEN true ELSE false END FROM t",
4645 1,
4646 0,
4647 1,
4648 Some("COALESCE(X > 0, FALSE)"),
4649 ),
4650 (
4652 "SELECT CASE WHEN x > 0 THEN false ELSE true END FROM t",
4653 1,
4654 0,
4655 1,
4656 Some("NOT COALESCE(X > 0, FALSE)"),
4657 ),
4658 (
4660 "SELECT CASE WHEN x IS NULL THEN 0 ELSE x END FROM t",
4661 1,
4662 0,
4663 1,
4664 Some("COALESCE(X, 0)"),
4665 ),
4666 (
4668 "SELECT CASE WHEN x = 1 THEN 'a' WHEN x = 2 THEN 'b' END FROM t",
4669 0,
4670 0,
4671 0,
4672 None,
4673 ),
4674 ];
4675
4676 for (sql, before, after, fix_count, expected_text) in cases {
4677 assert_rule_case(sql, issue_codes::LINT_ST_002, before, after, fix_count);
4678
4679 if let Some(expected) = expected_text {
4680 let out = apply_lint_fixes(sql, Dialect::Generic, &[]).expect("fix result");
4681 assert!(
4682 out.sql.to_ascii_uppercase().contains(expected),
4683 "expected {expected:?} in fixed SQL, got: {}",
4684 out.sql
4685 );
4686 }
4687 }
4688 }
4689
4690 #[test]
4691 fn sqlfluff_st006_cases_are_fixed_or_unchanged() {
4692 let cases = [
4693 ("SELECT a + 1, a FROM t", 1, 0, 1, Some("A,\n A + 1")),
4694 (
4695 "SELECT a + 1, b + 2, a FROM t",
4696 1,
4697 0,
4698 1,
4699 Some("A,\n A + 1,\n B + 2"),
4700 ),
4701 (
4702 "SELECT a + 1, b AS b_alias FROM t",
4703 1,
4704 0,
4705 1,
4706 Some("B AS B_ALIAS,\n A + 1"),
4707 ),
4708 ("SELECT a, b + 1 FROM t", 0, 0, 0, None),
4709 ("SELECT a + 1, b + 2 FROM t", 0, 0, 0, None),
4710 ];
4711
4712 for (sql, before, after, fix_count, expected_text) in cases {
4713 assert_rule_case(sql, issue_codes::LINT_ST_006, before, after, fix_count);
4714
4715 if let Some(expected) = expected_text {
4716 let out = apply_core_only_fixes(sql);
4717 assert!(
4718 out.sql.to_ascii_uppercase().contains(expected),
4719 "expected {expected:?} in fixed SQL, got: {}",
4720 out.sql
4721 );
4722 }
4723 }
4724 }
4725
4726 #[test]
4727 fn sqlfluff_st008_cases_are_fixed_or_unchanged() {
4728 let cases = [
4729 (
4730 "SELECT DISTINCT(a) FROM t",
4731 1,
4732 0,
4733 1,
4734 Some("SELECT DISTINCT A"),
4735 ),
4736 ("SELECT DISTINCT a FROM t", 0, 0, 0, None),
4737 ("SELECT a FROM t", 0, 0, 0, None),
4738 ];
4739
4740 for (sql, before, after, fix_count, expected_text) in cases {
4741 assert_rule_case(sql, issue_codes::LINT_ST_008, before, after, fix_count);
4742
4743 if let Some(expected) = expected_text {
4744 let out = apply_lint_fixes(sql, Dialect::Generic, &[]).expect("fix result");
4745 assert!(
4746 out.sql.to_ascii_uppercase().contains(expected),
4747 "expected {expected:?} in fixed SQL, got: {}",
4748 out.sql
4749 );
4750 }
4751 }
4752 }
4753
4754 #[test]
4755 fn sqlfluff_st009_cases_are_fixed_or_unchanged() {
4756 let cases = [
4757 (
4758 "SELECT foo.a, bar.b FROM foo LEFT JOIN bar ON bar.a = foo.a",
4759 1,
4760 0,
4761 1,
4762 Some("ON FOO.A = BAR.A"),
4763 ),
4764 (
4765 "SELECT foo.a, foo.b, bar.c FROM foo LEFT JOIN bar ON bar.a = foo.a AND bar.b = foo.b",
4766 1,
4767 1,
4768 0,
4769 None,
4770 ),
4771 (
4772 "SELECT foo.a, bar.b FROM foo LEFT JOIN bar ON foo.a = bar.a",
4773 0,
4774 0,
4775 0,
4776 None,
4777 ),
4778 (
4779 "SELECT foo.a, bar.b FROM foo LEFT JOIN bar ON bar.b = a",
4780 0,
4781 0,
4782 0,
4783 None,
4784 ),
4785 (
4786 "SELECT foo.a, bar.b FROM foo AS x LEFT JOIN bar AS y ON y.a = x.a",
4787 1,
4788 0,
4789 1,
4790 Some("ON X.A = Y.A"),
4791 ),
4792 ];
4793
4794 for (sql, before, after, fix_count, expected_text) in cases {
4795 if before == after && fix_count == 0 {
4796 let initial = lint_rule_count(sql, issue_codes::LINT_ST_009);
4797 assert_eq!(
4798 initial,
4799 before,
4800 "unexpected initial lint count for {} in SQL: {}",
4801 issue_codes::LINT_ST_009,
4802 sql
4803 );
4804
4805 let out = apply_lint_fixes(sql, Dialect::Generic, &[]).expect("fix result");
4806 assert_eq!(
4807 fix_count_for_code(&out.counts, issue_codes::LINT_ST_009),
4808 0,
4809 "unexpected fix count for {} in SQL: {}",
4810 issue_codes::LINT_ST_009,
4811 sql
4812 );
4813 let after_count = lint_rule_count(&out.sql, issue_codes::LINT_ST_009);
4814 assert_eq!(
4815 after_count,
4816 after,
4817 "unexpected lint count after fix for {}. SQL: {}",
4818 issue_codes::LINT_ST_009,
4819 out.sql
4820 );
4821 } else {
4822 assert_rule_case(sql, issue_codes::LINT_ST_009, before, after, fix_count);
4823 }
4824
4825 if let Some(expected) = expected_text {
4826 let out = apply_lint_fixes(sql, Dialect::Generic, &[]).expect("fix result");
4827 assert!(
4828 out.sql.to_ascii_uppercase().contains(expected),
4829 "expected {expected:?} in fixed SQL, got: {}",
4830 out.sql
4831 );
4832 }
4833 }
4834 }
4835
4836 #[test]
4837 fn sqlfluff_st007_cases_are_fixed_or_unchanged() {
4838 let cases = [
4839 (
4840 "SELECT * FROM a JOIN b USING (id)",
4841 1,
4842 0,
4843 1,
4844 Some("ON A.ID = B.ID"),
4845 ),
4846 (
4847 "SELECT * FROM a AS x JOIN b AS y USING (id)",
4848 1,
4849 0,
4850 1,
4851 Some("ON X.ID = Y.ID"),
4852 ),
4853 (
4854 "SELECT * FROM a JOIN b USING (id, tenant_id)",
4855 1,
4856 0,
4857 1,
4858 Some("ON A.ID = B.ID AND A.TENANT_ID = B.TENANT_ID"),
4859 ),
4860 ("SELECT * FROM a JOIN b ON a.id = b.id", 0, 0, 0, None),
4861 ];
4862
4863 for (sql, before, after, fix_count, expected_text) in cases {
4864 assert_rule_case(sql, issue_codes::LINT_ST_007, before, after, fix_count);
4865
4866 if let Some(expected) = expected_text {
4867 let out = apply_lint_fixes(sql, Dialect::Generic, &[]).expect("fix result");
4868 assert!(
4869 out.sql.to_ascii_uppercase().contains(expected),
4870 "expected {expected:?} in fixed SQL, got: {}",
4871 out.sql
4872 );
4873 }
4874 }
4875 }
4876
4877 #[test]
4878 fn sqlfluff_st004_cases_are_fixed_or_unchanged() {
4879 let cases = [
4880 (
4881 "SELECT CASE WHEN species = 'Rat' THEN 'Squeak' ELSE CASE WHEN species = 'Dog' THEN 'Woof' END END AS sound FROM mytable",
4882 1,
4883 1,
4884 0,
4885 ),
4886 (
4887 "SELECT CASE WHEN species = 'Rat' THEN 'Squeak' ELSE CASE WHEN species = 'Dog' THEN 'Woof' WHEN species = 'Mouse' THEN 'Squeak' ELSE 'Other' END END AS sound FROM mytable",
4888 1,
4889 1,
4890 0,
4891 ),
4892 (
4893 "SELECT CASE WHEN species = 'Rat' THEN CASE WHEN colour = 'Black' THEN 'Growl' WHEN colour = 'Grey' THEN 'Squeak' END END AS sound FROM mytable",
4894 0,
4895 0,
4896 0,
4897 ),
4898 (
4899 "SELECT CASE WHEN day_of_month IN (11, 12, 13) THEN 'TH' ELSE CASE MOD(day_of_month, 10) WHEN 1 THEN 'ST' WHEN 2 THEN 'ND' WHEN 3 THEN 'RD' ELSE 'TH' END END AS ordinal_suffix FROM calendar",
4900 0,
4901 0,
4902 0,
4903 ),
4904 (
4905 "SELECT CASE x WHEN 0 THEN 'zero' WHEN 5 THEN 'five' ELSE CASE x WHEN 10 THEN 'ten' WHEN 20 THEN 'twenty' ELSE 'other' END END FROM tab_a",
4906 1,
4907 1,
4908 0,
4909 ),
4910 ];
4911
4912 for (sql, before, after, fix_count) in cases {
4913 assert_rule_case(sql, issue_codes::LINT_ST_004, before, after, fix_count);
4914 }
4915 }
4916
4917 #[test]
4918 fn sqlfluff_cv003_cases_are_fixed_or_unchanged() {
4919 let cases = [
4920 ("SELECT a FROM foo WHERE a IS NULL", 0, 0, 0, None),
4921 ("SELECT a FROM foo WHERE a IS NOT NULL", 0, 0, 0, None),
4922 (
4923 "SELECT a FROM foo WHERE a <> NULL",
4924 1,
4925 0,
4926 1,
4927 Some("WHERE A IS NOT NULL"),
4928 ),
4929 (
4930 "SELECT a FROM foo WHERE a <> NULL AND b != NULL AND c = 'foo'",
4931 2,
4932 0,
4933 2,
4934 Some("A IS NOT NULL AND B IS NOT NULL"),
4935 ),
4936 (
4937 "SELECT a FROM foo WHERE a = NULL",
4938 1,
4939 0,
4940 1,
4941 Some("WHERE A IS NULL"),
4942 ),
4943 (
4944 "SELECT a FROM foo WHERE a=NULL",
4945 1,
4946 0,
4947 1,
4948 Some("WHERE A IS NULL"),
4949 ),
4950 (
4951 "SELECT a FROM foo WHERE a = b OR (c > d OR e = NULL)",
4952 1,
4953 0,
4954 1,
4955 Some("OR E IS NULL"),
4956 ),
4957 ("UPDATE table1 SET col = NULL WHERE col = ''", 0, 0, 0, None),
4958 ];
4959
4960 for (sql, before, after, fix_count, expected_text) in cases {
4961 assert_rule_case(sql, issue_codes::LINT_CV_005, before, after, fix_count);
4962
4963 if let Some(expected) = expected_text {
4964 let out = apply_lint_fixes(sql, Dialect::Generic, &[]).expect("fix result");
4965 assert!(
4966 out.sql.to_ascii_uppercase().contains(expected),
4967 "expected {expected:?} in fixed SQL, got: {}",
4968 out.sql
4969 );
4970 }
4971 }
4972 }
4973
4974 #[test]
4975 fn sqlfluff_cv001_cases_are_fixed_or_unchanged() {
4976 let cases = [
4977 ("SELECT coalesce(foo, 0) AS bar FROM baz", 0, 0, 0),
4978 ("SELECT ifnull(foo, 0) AS bar FROM baz", 1, 0, 1),
4979 ("SELECT nvl(foo, 0) AS bar FROM baz", 1, 0, 1),
4980 (
4981 "SELECT CASE WHEN x IS NULL THEN 'default' ELSE x END FROM t",
4982 0,
4983 0,
4984 0,
4985 ),
4986 ];
4987
4988 for (sql, before, after, fix_count) in cases {
4989 assert_rule_case(sql, issue_codes::LINT_CV_002, before, after, fix_count);
4990 }
4991 }
4992
4993 #[test]
4994 fn sqlfluff_cv003_trailing_comma_cases_are_fixed_or_unchanged() {
4995 let cases = [
4996 ("SELECT a, FROM t", 1, 0, 1),
4997 ("SELECT a , FROM t", 1, 0, 1),
4998 ("SELECT a FROM t", 0, 0, 0),
4999 ];
5000
5001 for (sql, before, after, fix_count) in cases {
5002 assert_rule_case(sql, issue_codes::LINT_CV_003, before, after, fix_count);
5003 }
5004 }
5005
5006 #[test]
5007 fn sqlfluff_cv001_not_equal_style_cases_are_fixed_or_unchanged() {
5008 let cases = [
5009 ("SELECT * FROM t WHERE a <> b AND c != d", 1, 0, 1),
5010 ("SELECT * FROM t WHERE a != b", 0, 0, 0),
5011 ];
5012
5013 for (sql, before, after, fix_count) in cases {
5014 assert_rule_case(sql, issue_codes::LINT_CV_001, before, after, fix_count);
5015 }
5016 }
5017
5018 #[test]
5019 fn sqlfluff_cv008_cases_are_fixed_or_unchanged() {
5020 let cases: [(&str, usize, usize, usize, Option<&str>); 4] = [
5021 ("SELECT * FROM a RIGHT JOIN b ON a.id = b.id", 1, 1, 0, None),
5022 (
5023 "SELECT a.id FROM a JOIN b ON a.id = b.id RIGHT JOIN c ON b.id = c.id",
5024 1,
5025 1,
5026 0,
5027 None,
5028 ),
5029 (
5030 "SELECT a.id FROM a RIGHT JOIN b ON a.id = b.id RIGHT JOIN c ON b.id = c.id",
5031 2,
5032 2,
5033 0,
5034 None,
5035 ),
5036 ("SELECT * FROM a LEFT JOIN b ON a.id = b.id", 0, 0, 0, None),
5037 ];
5038
5039 for (sql, before, after, fix_count, expected_text) in cases {
5040 assert_rule_case(sql, issue_codes::LINT_CV_008, before, after, fix_count);
5041
5042 if let Some(expected) = expected_text {
5043 let out = apply_lint_fixes(sql, Dialect::Generic, &[]).expect("fix result");
5044 assert!(
5045 out.sql.to_ascii_uppercase().contains(expected),
5046 "expected {expected:?} in fixed SQL, got: {}",
5047 out.sql
5048 );
5049 }
5050 }
5051 }
5052
5053 #[test]
5054 fn sqlfluff_cv007_cases_are_fixed_or_unchanged() {
5055 let cases = [
5056 ("(SELECT 1)", 1, 0, 1),
5057 ("((SELECT 1))", 1, 0, 1),
5058 ("SELECT 1", 0, 0, 0),
5059 ];
5060
5061 for (sql, before, after, fix_count) in cases {
5062 assert_rule_case(sql, issue_codes::LINT_CV_007, before, after, fix_count);
5063 }
5064 }
5065
5066 #[test]
5067 fn cv007_fix_respects_disabled_rules() {
5068 let sql = "(SELECT 1)\n";
5069 let out = apply_lint_fixes(
5070 sql,
5071 Dialect::Generic,
5072 &[issue_codes::LINT_CV_007.to_string()],
5073 )
5074 .expect("fix result");
5075 assert_eq!(out.sql, sql);
5076 assert_eq!(out.counts.get(issue_codes::LINT_CV_007), 0);
5077 }
5078
5079 #[test]
5080 fn cv010_fix_converts_double_to_single_in_bigquery() {
5081 let lint_config = LintConfig {
5082 enabled: true,
5083 disabled_rules: vec![],
5084 rule_configs: std::collections::BTreeMap::from([(
5085 "convention.quoted_literals".to_string(),
5086 serde_json::json!({"preferred_quoted_literal_style": "single_quotes"}),
5087 )]),
5088 };
5089 let sql = "SELECT \"abc\"";
5091 let before = lint_rule_count_with_config_in_dialect(
5092 sql,
5093 issue_codes::LINT_CV_010,
5094 Dialect::Bigquery,
5095 &lint_config,
5096 );
5097 assert_eq!(
5098 before, 1,
5099 "CV10 should flag double-quoted string in BigQuery with single_quotes preference"
5100 );
5101
5102 let out = apply_lint_fixes_with_lint_config(sql, Dialect::Bigquery, &lint_config)
5103 .expect("fix result");
5104 assert!(
5105 out.sql.contains("'abc'"),
5106 "expected double-quoted string to be converted to single-quoted: {}",
5107 out.sql
5108 );
5109 }
5110
5111 #[test]
5112 fn cv011_cast_preference_rewrites_double_colon_style() {
5113 let lint_config = LintConfig {
5114 enabled: true,
5115 disabled_rules: vec![],
5116 rule_configs: std::collections::BTreeMap::from([(
5117 "convention.casting_style".to_string(),
5118 serde_json::json!({"preferred_type_casting_style": "cast"}),
5119 )]),
5120 };
5121 let sql = "SELECT amount::INT FROM t";
5122 assert_rule_case_with_config(sql, issue_codes::LINT_CV_011, 1, 0, 1, &lint_config);
5123
5124 let out = apply_fix_with_config(sql, &lint_config);
5125 assert!(
5126 out.sql.to_ascii_uppercase().contains("CAST(AMOUNT AS INT)"),
5127 "expected CAST(...) rewrite for CV_011 fix: {}",
5128 out.sql
5129 );
5130 }
5131
5132 #[test]
5133 fn cv011_shorthand_preference_rewrites_cast_style_when_safe() {
5134 let lint_config = LintConfig {
5135 enabled: true,
5136 disabled_rules: vec![],
5137 rule_configs: std::collections::BTreeMap::from([(
5138 "LINT_CV_011".to_string(),
5139 serde_json::json!({"preferred_type_casting_style": "shorthand"}),
5140 )]),
5141 };
5142 let sql = "SELECT CAST(amount AS INT) FROM t";
5143 assert_rule_case_with_config(sql, issue_codes::LINT_CV_011, 1, 0, 1, &lint_config);
5144
5145 let out = apply_fix_with_config(sql, &lint_config);
5146 assert!(
5147 out.sql.to_ascii_uppercase().contains("AMOUNT::INT"),
5148 "expected :: rewrite for CV_011 fix: {}",
5149 out.sql
5150 );
5151 }
5152
5153 #[test]
5154 fn sqlfluff_st012_cases_are_fixed_or_unchanged() {
5155 let cases = [
5156 ("SELECT 1;;", 1, 0, 1),
5157 ("SELECT 1;\n \t ;", 1, 0, 1),
5158 ("SELECT 1;", 0, 0, 0),
5159 ];
5160
5161 for (sql, before, after, fix_count) in cases {
5162 assert_rule_case(sql, issue_codes::LINT_ST_012, before, after, fix_count);
5163 }
5164 }
5165
5166 #[test]
5167 fn sqlfluff_st002_cases_are_fixed_or_unchanged() {
5168 let cases = [
5169 ("SELECT CASE WHEN x > 1 THEN 'a' ELSE NULL END FROM t", 1, 0, 1),
5170 (
5171 "SELECT CASE name WHEN 'cat' THEN 'meow' WHEN 'dog' THEN 'woof' ELSE NULL END FROM t",
5172 1,
5173 0,
5174 1,
5175 ),
5176 (
5177 "SELECT CASE WHEN x = 1 THEN 'a' WHEN x = 2 THEN 'b' WHEN x = 3 THEN 'c' ELSE NULL END FROM t",
5178 1,
5179 0,
5180 1,
5181 ),
5182 (
5183 "SELECT CASE WHEN x > 0 THEN CASE WHEN y > 0 THEN 'pos' ELSE NULL END ELSE NULL END FROM t",
5184 2,
5185 0,
5186 2,
5187 ),
5188 (
5189 "SELECT * FROM t WHERE (CASE WHEN x > 0 THEN 1 ELSE NULL END) IS NOT NULL",
5190 1,
5191 0,
5192 1,
5193 ),
5194 (
5195 "WITH cte AS (SELECT CASE WHEN x > 0 THEN 'yes' ELSE NULL END AS flag FROM t) SELECT * FROM cte",
5196 1,
5197 0,
5198 1,
5199 ),
5200 ("SELECT CASE WHEN x > 1 THEN 'a' END FROM t", 0, 0, 0),
5201 (
5202 "SELECT CASE name WHEN 'cat' THEN 'meow' ELSE UPPER(name) END FROM t",
5203 0,
5204 0,
5205 0,
5206 ),
5207 ("SELECT CASE WHEN x > 1 THEN 'a' ELSE 'b' END FROM t", 0, 0, 0),
5208 ];
5209
5210 for (sql, before, after, fix_count) in cases {
5211 assert_rule_case(sql, issue_codes::LINT_ST_001, before, after, fix_count);
5212 }
5213 }
5214
5215 #[test]
5216 fn count_style_cases_are_fixed_or_unchanged() {
5217 let cases = [
5218 ("SELECT COUNT(1) FROM t", 1, 0, 1),
5219 (
5220 "SELECT col FROM t GROUP BY col HAVING COUNT(1) > 5",
5221 1,
5222 0,
5223 1,
5224 ),
5225 (
5226 "SELECT * FROM t WHERE id IN (SELECT COUNT(1) FROM t2 GROUP BY col)",
5227 1,
5228 0,
5229 1,
5230 ),
5231 ("SELECT COUNT(1), COUNT(1) FROM t", 2, 0, 2),
5232 (
5233 "WITH cte AS (SELECT COUNT(1) AS cnt FROM t) SELECT * FROM cte",
5234 1,
5235 0,
5236 1,
5237 ),
5238 ("SELECT COUNT(*) FROM t", 0, 0, 0),
5239 ("SELECT COUNT(id) FROM t", 0, 0, 0),
5240 ("SELECT COUNT(0) FROM t", 1, 0, 1),
5241 ("SELECT COUNT(01) FROM t", 1, 0, 1),
5242 ("SELECT COUNT(DISTINCT id) FROM t", 0, 0, 0),
5243 ];
5244
5245 for (sql, before, after, fix_count) in cases {
5246 assert_rule_case(sql, issue_codes::LINT_CV_004, before, after, fix_count);
5247 }
5248 }
5249
5250 #[test]
5251 fn safe_mode_blocks_template_tag_edits_but_applies_non_template_fixes() {
5252 let sql = "SELECT '{{foo}}' AS templated, COUNT(1) FROM t";
5253 let out = apply_lint_fixes_with_options(
5254 sql,
5255 Dialect::Generic,
5256 &default_lint_config(),
5257 FixOptions {
5258 include_unsafe_fixes: false,
5259 include_rewrite_candidates: true,
5260 },
5261 )
5262 .expect("fix result");
5263
5264 assert!(
5265 out.sql.contains("{{foo}}"),
5266 "template tag bytes should be preserved in safe mode: {}",
5267 out.sql
5268 );
5269 assert!(
5270 out.sql.to_ascii_uppercase().contains("COUNT(*)"),
5271 "non-template safe fixes should still apply: {}",
5272 out.sql
5273 );
5274 assert!(
5275 out.skipped_counts.protected_range_blocked > 0,
5276 "template-tag edits should be blocked in safe mode"
5277 );
5278 }
5279
5280 #[test]
5281 fn unsafe_mode_allows_template_tag_edits() {
5282 let sql = "SELECT '{{foo}}' AS templated, COUNT(1) FROM t";
5283 let out = apply_lint_fixes_with_options(
5284 sql,
5285 Dialect::Generic,
5286 &default_lint_config(),
5287 FixOptions {
5288 include_unsafe_fixes: true,
5289 include_rewrite_candidates: false,
5290 },
5291 )
5292 .expect("fix result");
5293
5294 assert!(
5295 out.sql.contains("{{ foo }}"),
5296 "unsafe mode should allow template-tag formatting fixes: {}",
5297 out.sql
5298 );
5299 assert!(
5300 out.sql.to_ascii_uppercase().contains("COUNT(*)"),
5301 "other fixes should still apply: {}",
5302 out.sql
5303 );
5304 }
5305
5306 #[test]
5307 fn comments_are_not_globally_skipped() {
5308 let sql = "-- keep this comment\nSELECT COUNT(1) FROM t";
5309 let out = apply_lint_fixes_with_options(
5310 sql,
5311 Dialect::Generic,
5312 &default_lint_config(),
5313 FixOptions {
5314 include_unsafe_fixes: false,
5315 include_rewrite_candidates: false,
5316 },
5317 )
5318 .expect("fix result");
5319 assert!(
5320 !out.skipped_due_to_comments,
5321 "comment presence should not skip all fixes"
5322 );
5323 assert!(
5324 out.sql.contains("-- keep this comment"),
5325 "comment text must be preserved: {}",
5326 out.sql
5327 );
5328 assert!(
5329 out.sql.to_ascii_uppercase().contains("COUNT(*)"),
5330 "non-comment region should still be fixable: {}",
5331 out.sql
5332 );
5333 }
5334
5335 #[test]
5336 fn mysql_hash_comments_are_not_globally_skipped() {
5337 let sql = "# keep this comment\nSELECT COUNT(1) FROM t";
5338 let out = apply_lint_fixes_with_options(
5339 sql,
5340 Dialect::Mysql,
5341 &default_lint_config(),
5342 FixOptions {
5343 include_unsafe_fixes: false,
5344 include_rewrite_candidates: false,
5345 },
5346 )
5347 .expect("fix result");
5348 assert!(
5349 !out.skipped_due_to_comments,
5350 "comment presence should not skip all fixes"
5351 );
5352 assert!(
5353 out.sql.contains("# keep this comment"),
5354 "comment text must be preserved: {}",
5355 out.sql
5356 );
5357 assert!(
5358 out.sql.to_ascii_uppercase().contains("COUNT(*)"),
5359 "non-comment region should still be fixable: {}",
5360 out.sql
5361 );
5362 }
5363
5364 #[test]
5365 fn does_not_treat_double_quoted_comment_markers_as_comments() {
5366 let sql = "SELECT \"a--b\", \"x/*y\" FROM t";
5367 assert!(!contains_comment_markers(sql, Dialect::Generic));
5368 }
5369
5370 #[test]
5371 fn does_not_treat_backtick_or_bracketed_markers_as_comments() {
5372 let sql = "SELECT `a--b`, [x/*y] FROM t";
5373 assert!(!contains_comment_markers(sql, Dialect::Mysql));
5374 }
5375
5376 #[test]
5377 fn fix_mode_does_not_skip_double_quoted_markers() {
5378 let sql = "SELECT \"a--b\", COUNT(1) FROM t";
5379 let out = apply_lint_fixes(sql, Dialect::Generic, &[]).expect("fix result");
5380 assert!(!out.skipped_due_to_comments);
5381 }
5382
5383 #[test]
5384 fn fix_mode_does_not_skip_backtick_markers() {
5385 let sql = "SELECT `a--b`, COUNT(1) FROM t";
5386 let out = apply_lint_fixes(sql, Dialect::Mysql, &[]).expect("fix result");
5387 assert!(!out.skipped_due_to_comments);
5388 }
5389
5390 #[test]
5391 fn planner_blocks_protected_ranges_and_applies_non_overlapping_edits() {
5392 let sql = "SELECT '{{foo}}' AS templated, 1";
5393 let protected = collect_comment_protected_ranges(sql, Dialect::Generic, true);
5394 let template_idx = sql.find("{{foo}}").expect("template exists");
5395 let one_idx = sql.rfind('1').expect("digit exists");
5396
5397 let planned = plan_fix_candidates(
5398 sql,
5399 vec![
5400 FixCandidate {
5401 start: template_idx,
5402 end: template_idx + "{{foo}}".len(),
5403 replacement: String::new(),
5404 applicability: FixCandidateApplicability::Safe,
5405 source: FixCandidateSource::PrimaryRewrite,
5406 rule_code: None,
5407 },
5408 FixCandidate {
5409 start: one_idx,
5410 end: one_idx + 1,
5411 replacement: "2".to_string(),
5412 applicability: FixCandidateApplicability::Safe,
5413 source: FixCandidateSource::PrimaryRewrite,
5414 rule_code: None,
5415 },
5416 ],
5417 &protected,
5418 false,
5419 );
5420
5421 let applied = apply_planned_edits(sql, &planned.edits);
5422 assert!(
5423 applied.contains("{{foo}}"),
5424 "template span should remain protected: {applied}"
5425 );
5426 assert!(
5427 applied.ends_with("2"),
5428 "expected non-overlapping edit: {applied}"
5429 );
5430 assert_eq!(planned.skipped.protected_range_blocked, 1);
5431 }
5432
5433 #[test]
5434 fn planner_is_deterministic_for_conflicting_candidates() {
5435 let sql = "SELECT 1";
5436 let one_idx = sql.rfind('1').expect("digit exists");
5437
5438 let left_first = plan_fix_candidates(
5439 sql,
5440 vec![
5441 FixCandidate {
5442 start: one_idx,
5443 end: one_idx + 1,
5444 replacement: "9".to_string(),
5445 applicability: FixCandidateApplicability::Safe,
5446 source: FixCandidateSource::PrimaryRewrite,
5447 rule_code: None,
5448 },
5449 FixCandidate {
5450 start: one_idx,
5451 end: one_idx + 1,
5452 replacement: "2".to_string(),
5453 applicability: FixCandidateApplicability::Safe,
5454 source: FixCandidateSource::PrimaryRewrite,
5455 rule_code: None,
5456 },
5457 ],
5458 &[],
5459 false,
5460 );
5461 let right_first = plan_fix_candidates(
5462 sql,
5463 vec![
5464 FixCandidate {
5465 start: one_idx,
5466 end: one_idx + 1,
5467 replacement: "2".to_string(),
5468 applicability: FixCandidateApplicability::Safe,
5469 source: FixCandidateSource::PrimaryRewrite,
5470 rule_code: None,
5471 },
5472 FixCandidate {
5473 start: one_idx,
5474 end: one_idx + 1,
5475 replacement: "9".to_string(),
5476 applicability: FixCandidateApplicability::Safe,
5477 source: FixCandidateSource::PrimaryRewrite,
5478 rule_code: None,
5479 },
5480 ],
5481 &[],
5482 false,
5483 );
5484
5485 let left_sql = apply_planned_edits(sql, &left_first.edits);
5486 let right_sql = apply_planned_edits(sql, &right_first.edits);
5487 assert_eq!(left_sql, "SELECT 2");
5488 assert_eq!(left_sql, right_sql);
5489 assert_eq!(left_first.skipped.overlap_conflict_blocked, 1);
5490 assert_eq!(right_first.skipped.overlap_conflict_blocked, 1);
5491 }
5492
5493 #[test]
5494 fn core_autofix_candidates_are_collected_and_applied() {
5495 let sql = "SELECT 1";
5496 let one_idx = sql.rfind('1').expect("digit exists");
5497 let issues = vec![serde_json::json!({
5498 "code": issue_codes::LINT_CV_004,
5499 "span": { "start": one_idx, "end": one_idx + 1 },
5500 "autofix": {
5501 "applicability": "safe",
5502 "edits": [
5503 {
5504 "start": one_idx,
5505 "end": one_idx + 1,
5506 "replacement": "2"
5507 }
5508 ]
5509 }
5510 })];
5511 let candidates = build_fix_candidates_from_issue_values(sql, &issues);
5512
5513 assert_eq!(candidates.len(), 1);
5514 let planned = plan_fix_candidates(sql, candidates, &[], false);
5515 let applied = apply_planned_edits(sql, &planned.edits);
5516 assert_eq!(applied, "SELECT 2");
5517 }
5518
5519 #[test]
5520 fn st002_core_autofix_candidates_apply_cleanly_in_safe_mode() {
5521 let sql = "SELECT CASE WHEN x > 0 THEN true ELSE false END FROM t\n";
5522 let issues = lint_issues(sql, Dialect::Generic, &default_lint_config());
5523 let candidates = build_fix_candidates_from_issue_autofixes(sql, &issues);
5524 assert!(
5525 candidates
5526 .iter()
5527 .any(|candidate| candidate.rule_code.as_deref() == Some(issue_codes::LINT_ST_002)),
5528 "expected ST002 core candidate from lint issues: {candidates:?}"
5529 );
5530
5531 let protected = collect_comment_protected_ranges(sql, Dialect::Generic, true);
5532 let planned = plan_fix_candidates(sql, candidates, &protected, false);
5533 let applied = apply_planned_edits(sql, &planned.edits);
5534 assert_eq!(
5535 applied, "SELECT coalesce(x > 0, false) FROM t\n",
5536 "unexpected ST002 planned edits with skipped={:?}",
5537 planned.skipped
5538 );
5539 }
5540
5541 #[test]
5542 fn incremental_core_plan_applies_st009_even_when_not_top_priority() {
5543 let sql = "select foo.a, bar.b from foo left join bar on bar.a = foo.a";
5544 let lint_config = default_lint_config();
5545 let before_counts = lint_rule_counts(sql, Dialect::Generic, &lint_config);
5546 assert_eq!(
5547 before_counts
5548 .get(issue_codes::LINT_ST_009)
5549 .copied()
5550 .unwrap_or(0),
5551 1
5552 );
5553
5554 let out = try_incremental_core_fix_plan(
5555 sql,
5556 Dialect::Generic,
5557 &lint_config,
5558 &before_counts,
5559 None,
5560 false,
5561 24,
5562 usize::MAX,
5563 )
5564 .expect("expected incremental ST009 fix");
5565 assert!(
5566 out.sql.contains("foo.a = bar.a"),
5567 "expected ST009 join condition reorder, got: {}",
5568 out.sql
5569 );
5570
5571 let after_counts = lint_rule_counts(&out.sql, Dialect::Generic, &lint_config);
5572 assert_eq!(
5573 after_counts
5574 .get(issue_codes::LINT_ST_009)
5575 .copied()
5576 .unwrap_or(0),
5577 0
5578 );
5579 }
5580
5581 #[test]
5582 fn cached_pre_lint_state_matches_uncached_next_pass_behavior() {
5583 let sql = "SELECT 1 UNION SELECT 2";
5584 let lint_config = default_lint_config();
5585 let fix_options = FixOptions {
5586 include_unsafe_fixes: false,
5587 include_rewrite_candidates: false,
5588 };
5589
5590 let first_pass = apply_lint_fixes_with_options_and_lint_state(
5591 sql,
5592 Dialect::Generic,
5593 &lint_config,
5594 fix_options,
5595 None,
5596 )
5597 .expect("first fix pass");
5598
5599 let second_cached = apply_lint_fixes_with_options_and_lint_state(
5600 &first_pass.outcome.sql,
5601 Dialect::Generic,
5602 &lint_config,
5603 fix_options,
5604 Some(first_pass.post_lint_state.clone()),
5605 )
5606 .expect("second cached pass");
5607 let second_uncached = apply_lint_fixes_with_options_and_lint_state(
5608 &first_pass.outcome.sql,
5609 Dialect::Generic,
5610 &lint_config,
5611 fix_options,
5612 None,
5613 )
5614 .expect("second uncached pass");
5615
5616 assert_eq!(second_cached.outcome.sql, second_uncached.outcome.sql);
5617 assert_eq!(second_cached.outcome.counts, second_uncached.outcome.counts);
5618 assert_eq!(
5619 second_cached.outcome.changed,
5620 second_uncached.outcome.changed
5621 );
5622 assert_eq!(
5623 second_cached.outcome.skipped_due_to_regression,
5624 second_uncached.outcome.skipped_due_to_regression
5625 );
5626 }
5627
5628 #[test]
5629 fn cp03_templated_case_emits_core_autofix_candidate() {
5630 let sql = "SELECT\n {{ \"greatest(a, b)\" }},\n GREATEST(i, j)\n";
5631 let config = lint_config_keep_only_rule(
5632 issue_codes::LINT_CP_003,
5633 LintConfig {
5634 enabled: true,
5635 disabled_rules: vec![],
5636 rule_configs: std::collections::BTreeMap::from([(
5637 "core".to_string(),
5638 serde_json::json!({"ignore_templated_areas": false}),
5639 )]),
5640 },
5641 );
5642 let issues = lint_issues(sql, Dialect::Ansi, &config);
5643 assert!(
5644 issues
5645 .iter()
5646 .any(|issue| { issue.code == issue_codes::LINT_CP_003 && issue.autofix.is_some() }),
5647 "expected CP03 issue with autofix metadata, got issues={issues:?}"
5648 );
5649
5650 let candidates = build_fix_candidates_from_issue_autofixes(sql, &issues);
5651 assert!(
5652 candidates.iter().any(|candidate| {
5653 candidate.rule_code.as_deref() == Some(issue_codes::LINT_CP_003)
5654 && &sql[candidate.start..candidate.end] == "GREATEST"
5655 && candidate.replacement == "greatest"
5656 }),
5657 "expected CP03 GREATEST candidate, got candidates={candidates:?}"
5658 );
5659 }
5660
5661 #[test]
5662 fn planner_prefers_core_autofix_over_rewrite_conflicts() {
5663 let sql = "SELECT 1";
5664 let one_idx = sql.rfind('1').expect("digit exists");
5665 let core_issue = serde_json::json!({
5666 "code": issue_codes::LINT_CV_004,
5667 "autofix": {
5668 "start": one_idx,
5669 "end": one_idx + 1,
5670 "replacement": "9",
5671 "applicability": "safe"
5672 }
5673 });
5674 let core_candidate = build_fix_candidates_from_issue_values(sql, &[core_issue])[0].clone();
5675 let rewrite_candidate = FixCandidate {
5676 start: one_idx,
5677 end: one_idx + 1,
5678 replacement: "2".to_string(),
5679 applicability: FixCandidateApplicability::Safe,
5680 source: FixCandidateSource::PrimaryRewrite,
5681 rule_code: None,
5682 };
5683
5684 let left_first = plan_fix_candidates(
5685 sql,
5686 vec![rewrite_candidate.clone(), core_candidate.clone()],
5687 &[],
5688 false,
5689 );
5690 let right_first =
5691 plan_fix_candidates(sql, vec![core_candidate, rewrite_candidate], &[], false);
5692
5693 let left_sql = apply_planned_edits(sql, &left_first.edits);
5694 let right_sql = apply_planned_edits(sql, &right_first.edits);
5695 assert_eq!(left_sql, "SELECT 9");
5696 assert_eq!(left_sql, right_sql);
5697 assert_eq!(left_first.skipped.overlap_conflict_blocked, 1);
5698 assert_eq!(right_first.skipped.overlap_conflict_blocked, 1);
5699 }
5700
5701 #[test]
5702 fn rewrite_mode_falls_back_to_core_plan_when_core_rule_is_not_improved() {
5703 let sql = "SELECT * FROM t WHERE a <> b AND c != d";
5706 let out = apply_lint_fixes_with_options(
5707 sql,
5708 Dialect::Generic,
5709 &default_lint_config(),
5710 FixOptions {
5711 include_unsafe_fixes: true,
5712 include_rewrite_candidates: true,
5713 },
5714 )
5715 .expect("fix result");
5716
5717 assert_eq!(fix_count_for_code(&out.counts, issue_codes::LINT_CV_001), 1);
5718 assert!(
5719 out.sql.contains("a <> b"),
5720 "expected CV001 style fix: {}",
5721 out.sql
5722 );
5723 assert!(
5724 out.sql.contains("c <> d"),
5725 "expected CV001 style fix: {}",
5726 out.sql
5727 );
5728 assert!(
5729 !out.sql.contains("!="),
5730 "expected no bang-style operator: {}",
5731 out.sql
5732 );
5733 }
5734
5735 #[test]
5736 fn core_autofix_applicability_is_mapped_to_existing_planner_logic() {
5737 let sql = "SELECT 1";
5738 let one_idx = sql.rfind('1').expect("digit exists");
5739 let issues = vec![
5740 serde_json::json!({
5741 "code": issue_codes::LINT_ST_005,
5742 "autofix": {
5743 "start": one_idx,
5744 "end": one_idx + 1,
5745 "replacement": "2",
5746 "applicability": "unsafe"
5747 }
5748 }),
5749 serde_json::json!({
5750 "code": issue_codes::LINT_ST_005,
5751 "autofix": {
5752 "start": one_idx,
5753 "end": one_idx + 1,
5754 "replacement": "3",
5755 "applicability": "display_only"
5756 }
5757 }),
5758 ];
5759 let candidates = build_fix_candidates_from_issue_values(sql, &issues);
5760
5761 assert_eq!(
5762 candidates[0].applicability,
5763 FixCandidateApplicability::Unsafe
5764 );
5765 assert_eq!(
5766 candidates[1].applicability,
5767 FixCandidateApplicability::DisplayOnly
5768 );
5769
5770 let planned_safe = plan_fix_candidates(sql, candidates.clone(), &[], false);
5771 assert_eq!(apply_planned_edits(sql, &planned_safe.edits), sql);
5772 assert_eq!(planned_safe.skipped.unsafe_skipped, 1);
5773 assert_eq!(planned_safe.skipped.display_only, 1);
5774
5775 let planned_unsafe = plan_fix_candidates(sql, candidates, &[], true);
5776 assert_eq!(apply_planned_edits(sql, &planned_unsafe.edits), "SELECT 2");
5777 assert_eq!(planned_unsafe.skipped.display_only, 1);
5778 }
5779
5780 #[test]
5781 fn planner_tracks_unsafe_and_display_only_skips() {
5782 let sql = "SELECT 1";
5783 let one_idx = sql.rfind('1').expect("digit exists");
5784 let planned = plan_fix_candidates(
5785 sql,
5786 vec![
5787 FixCandidate {
5788 start: one_idx,
5789 end: one_idx + 1,
5790 replacement: "2".to_string(),
5791 applicability: FixCandidateApplicability::Unsafe,
5792 source: FixCandidateSource::UnsafeFallback,
5793 rule_code: None,
5794 },
5795 FixCandidate {
5796 start: 0,
5797 end: 0,
5798 replacement: String::new(),
5799 applicability: FixCandidateApplicability::DisplayOnly,
5800 source: FixCandidateSource::DisplayHint,
5801 rule_code: None,
5802 },
5803 ],
5804 &[],
5805 false,
5806 );
5807 let applied = apply_planned_edits(sql, &planned.edits);
5808 assert_eq!(applied, sql);
5809 assert_eq!(planned.skipped.unsafe_skipped, 1);
5810 assert_eq!(planned.skipped.display_only, 1);
5811 }
5812
5813 #[test]
5814 fn does_not_collapse_independent_select_statements() {
5815 let sql = "SELECT 1; SELECT 2;";
5816 let out = apply_lint_fixes(sql, Dialect::Generic, &[]).expect("fix result");
5817 assert!(
5818 !out.sql.to_ascii_uppercase().contains("DISTINCT SELECT"),
5819 "auto-fix must preserve statement boundaries: {}",
5820 out.sql
5821 );
5822 let parsed = parse_sql_with_dialect(&out.sql, Dialect::Generic).expect("parse fixed sql");
5823 assert_eq!(
5824 parsed.len(),
5825 2,
5826 "auto-fix should preserve two independent statements"
5827 );
5828 }
5829
5830 #[test]
5831 fn subquery_to_cte_text_fix_applies() {
5832 let fixed = fix_subquery_to_cte("SELECT * FROM (SELECT 1) sub");
5833 assert_eq!(fixed, "WITH sub AS (SELECT 1) SELECT * FROM sub");
5834 }
5835
5836 #[test]
5837 fn st005_core_autofix_applies_in_unsafe_mode_with_from_config() {
5838 let sql = "SELECT * FROM (SELECT 1) sub";
5839 let lint_config = LintConfig {
5840 enabled: true,
5841 disabled_rules: vec![],
5842 rule_configs: std::collections::BTreeMap::from([(
5843 "structure.subquery".to_string(),
5844 serde_json::json!({"forbid_subquery_in": "from"}),
5845 )]),
5846 };
5847
5848 let fixed = apply_lint_fixes_with_options(
5849 sql,
5850 Dialect::Generic,
5851 &lint_config,
5852 FixOptions {
5853 include_unsafe_fixes: true,
5854 include_rewrite_candidates: false,
5855 },
5856 )
5857 .expect("fix result")
5858 .sql;
5859 assert!(
5860 fixed.to_ascii_uppercase().contains("WITH SUB AS"),
5861 "expected unsafe core ST005 autofix to rewrite to CTE, got: {fixed}"
5862 );
5863 }
5864
5865 #[test]
5866 fn subquery_to_cte_text_fix_handles_nested_parentheses() {
5867 let fixed = fix_subquery_to_cte("SELECT * FROM (SELECT COUNT(*) FROM t) sub");
5868 assert_eq!(
5869 fixed,
5870 "WITH sub AS (SELECT COUNT(*) FROM t) SELECT * FROM sub"
5871 );
5872 parse_sql_with_dialect(&fixed, Dialect::Generic).expect("fixed SQL should parse");
5873 }
5874
5875 #[test]
5876 fn st005_ast_fix_rewrites_simple_join_derived_subquery_to_cte() {
5877 let lint_config = LintConfig {
5878 enabled: true,
5879 disabled_rules: vec![issue_codes::LINT_AM_005.to_string()],
5880 rule_configs: std::collections::BTreeMap::new(),
5881 };
5882 let sql = "SELECT t.id FROM t JOIN (SELECT id FROM u) sub ON t.id = sub.id";
5883 assert_rule_case_with_config(sql, issue_codes::LINT_ST_005, 1, 0, 1, &lint_config);
5884
5885 let out = apply_fix_with_config(sql, &lint_config);
5886 assert!(
5887 out.sql.to_ascii_uppercase().contains("WITH SUB AS"),
5888 "expected AST ST_005 rewrite to emit CTE: {}",
5889 out.sql
5890 );
5891 }
5892
5893 #[test]
5894 fn st005_ast_fix_rewrites_simple_from_derived_subquery_with_config() {
5895 let lint_config = LintConfig {
5896 enabled: true,
5897 disabled_rules: vec![],
5898 rule_configs: std::collections::BTreeMap::from([(
5899 "structure.subquery".to_string(),
5900 serde_json::json!({"forbid_subquery_in": "from"}),
5901 )]),
5902 };
5903 let sql = "SELECT sub.id FROM (SELECT id FROM u) sub";
5904 assert_rule_case_with_config(sql, issue_codes::LINT_ST_005, 1, 0, 1, &lint_config);
5905
5906 let out = apply_fix_with_config(sql, &lint_config);
5907 assert!(
5908 out.sql.to_ascii_uppercase().contains("WITH SUB AS"),
5909 "expected FROM-derived ST_005 rewrite to emit CTE: {}",
5910 out.sql
5911 );
5912 }
5913
5914 #[test]
5915 fn consecutive_semicolon_fix_ignores_string_literal_content() {
5916 let sql = "SELECT 'a;;b' AS txt;;";
5917 let out = apply_lint_fixes_with_options(
5918 sql,
5919 Dialect::Generic,
5920 &default_lint_config(),
5921 FixOptions {
5922 include_unsafe_fixes: true,
5923 include_rewrite_candidates: false,
5924 },
5925 )
5926 .expect("fix result");
5927 assert!(
5928 out.sql.contains("'a;;b'"),
5929 "string literal content must be preserved: {}",
5930 out.sql
5931 );
5932 assert!(
5933 out.sql.trim_end().ends_with(';') && !out.sql.trim_end().ends_with(";;"),
5934 "trailing semicolon run should be collapsed to one terminator: {}",
5935 out.sql
5936 );
5937 }
5938
5939 #[test]
5940 fn consecutive_semicolon_fix_collapses_whitespace_separated_runs() {
5941 let out = apply_lint_fixes_with_options(
5942 "SELECT 1;\n \t ;",
5943 Dialect::Generic,
5944 &default_lint_config(),
5945 FixOptions {
5946 include_unsafe_fixes: true,
5947 include_rewrite_candidates: false,
5948 },
5949 )
5950 .expect("fix result");
5951 assert_eq!(out.sql.matches(';').count(), 1);
5952 }
5953
5954 #[test]
5955 fn lint_fix_subquery_with_function_call_is_parseable() {
5956 let sql = "SELECT * FROM (SELECT COUNT(*) FROM t) sub";
5957 let out = apply_lint_fixes(sql, Dialect::Generic, &[]).expect("fix result");
5958 assert!(
5959 !out.skipped_due_to_regression,
5960 "function-call subquery rewrite should not be treated as regression: {}",
5961 out.sql
5962 );
5963 parse_sql_with_dialect(&out.sql, Dialect::Generic).expect("fixed SQL should parse");
5964 }
5965
5966 #[test]
5967 fn distinct_parentheses_fix_preserves_valid_sql() {
5968 let sql = "SELECT DISTINCT(a) FROM t";
5969 let out = apply_lint_fixes(sql, Dialect::Generic, &[]).expect("fix result");
5970 assert!(
5971 !out.sql.contains("a)"),
5972 "unexpected dangling parenthesis after fix: {}",
5973 out.sql
5974 );
5975 parse_sql_with_dialect(&out.sql, Dialect::Generic).expect("fixed SQL should parse");
5976 }
5977
5978 #[test]
5979 fn not_equal_fix_does_not_rewrite_string_literals() {
5980 let sql = "SELECT '<>' AS x, a<>b, c!=d FROM t";
5981 let out = apply_lint_fixes_with_options(
5982 sql,
5983 Dialect::Generic,
5984 &default_lint_config(),
5985 FixOptions {
5986 include_unsafe_fixes: false,
5987 include_rewrite_candidates: false,
5988 },
5989 )
5990 .expect("fix result");
5991 assert!(
5992 out.sql.contains("'<>'"),
5993 "string literal should remain unchanged: {}",
5994 out.sql
5995 );
5996 let compact: String = out.sql.chars().filter(|ch| !ch.is_whitespace()).collect();
5997 let has_c_style = compact.contains("a!=b") && compact.contains("c!=d");
5998 let has_ansi_style = compact.contains("a<>b") && compact.contains("c<>d");
5999 assert!(
6000 has_c_style || has_ansi_style || compact.contains("a<>b") && compact.contains("c!=d"),
6001 "operator usage outside string literals should remain intact: {}",
6002 out.sql
6003 );
6004 }
6005
6006 #[test]
6007 fn spacing_fixes_do_not_rewrite_single_quoted_literals() {
6008 let operator_fixed = apply_lint_fixes_with_options(
6009 "SELECT payload->>'id', 'x=y' FROM t",
6010 Dialect::Generic,
6011 &default_lint_config(),
6012 FixOptions {
6013 include_unsafe_fixes: false,
6014 include_rewrite_candidates: false,
6015 },
6016 )
6017 .expect("operator spacing fix result")
6018 .sql;
6019 assert!(
6020 operator_fixed.contains("'x=y'"),
6021 "operator spacing must not mutate literals: {operator_fixed}"
6022 );
6023 assert!(
6024 operator_fixed.contains("payload ->>"),
6025 "operator spacing should still apply: {operator_fixed}"
6026 );
6027
6028 let comma_fixed = apply_lint_fixes_with_options(
6029 "SELECT a,b, 'x,y' FROM t",
6030 Dialect::Generic,
6031 &default_lint_config(),
6032 FixOptions {
6033 include_unsafe_fixes: false,
6034 include_rewrite_candidates: false,
6035 },
6036 )
6037 .expect("comma spacing fix result")
6038 .sql;
6039 assert!(
6040 comma_fixed.contains("'x,y'"),
6041 "comma spacing must not mutate literals: {comma_fixed}"
6042 );
6043 assert!(
6044 !comma_fixed.contains("a,b"),
6045 "comma spacing should still apply: {comma_fixed}"
6046 );
6047 }
6048
6049 #[test]
6050 fn keyword_newline_fix_does_not_rewrite_literals_or_quoted_identifiers() {
6051 let sql = "SELECT COUNT(1), 'hello FROM world', \"x WHERE y\" FROM t WHERE a = 1";
6052 let fixed = apply_lint_fixes(sql, Dialect::Generic, &[])
6053 .expect("fix result")
6054 .sql;
6055 assert!(
6056 fixed.contains("'hello FROM world'"),
6057 "single-quoted literal should remain unchanged: {fixed}"
6058 );
6059 assert!(
6060 fixed.contains("\"x WHERE y\""),
6061 "double-quoted identifier should remain unchanged: {fixed}"
6062 );
6063 assert!(
6064 !fixed.contains("hello\nFROM world"),
6065 "keyword newline fix must not inject newlines into literals: {fixed}"
6066 );
6067 }
6068
6069 #[test]
6070 fn cp04_fix_reduces_literal_capitalisation_violations() {
6071 assert_rule_case(
6073 "SELECT NULL, true, False FROM t",
6074 issue_codes::LINT_CP_004,
6075 2,
6076 0,
6077 2,
6078 );
6079 }
6080
6081 #[test]
6082 fn cp05_fix_reduces_type_capitalisation_violations() {
6083 assert_rule_case(
6085 "CREATE TABLE t (a INT, b VarChar(10));",
6086 issue_codes::LINT_CP_005,
6087 1,
6088 0,
6089 1,
6090 );
6091 }
6092
6093 #[test]
6094 fn cv06_fix_adds_missing_final_terminator() {
6095 assert_rule_case("SELECT 1 ;", issue_codes::LINT_CV_006, 1, 0, 1);
6096 }
6097
6098 #[test]
6099 fn lt03_fix_moves_trailing_operator_to_leading_position() {
6100 assert_rule_case("SELECT a +\n b FROM t", issue_codes::LINT_LT_003, 1, 0, 1);
6101 }
6102
6103 #[test]
6104 fn lt04_fix_moves_comma_around_templated_columns_in_ansi() {
6105 let leading_sql = "SELECT\n c1,\n {{ \"c2\" }} AS days_since\nFROM logs";
6106 let leading_config = lint_config_keep_only_rule(
6107 issue_codes::LINT_LT_004,
6108 LintConfig {
6109 enabled: true,
6110 disabled_rules: vec![],
6111 rule_configs: std::collections::BTreeMap::from([(
6112 "layout.commas".to_string(),
6113 serde_json::json!({"line_position": "leading"}),
6114 )]),
6115 },
6116 );
6117 let leading_issues = lint_issues(leading_sql, Dialect::Ansi, &leading_config);
6118 let leading_lt04 = leading_issues
6119 .iter()
6120 .find(|issue| issue.code == issue_codes::LINT_LT_004)
6121 .expect("expected LT04 issue before fix");
6122 assert!(
6123 leading_lt04.autofix.is_some(),
6124 "expected LT04 issue to carry autofix metadata in fix pipeline"
6125 );
6126 let leading_out = apply_lint_fixes_with_options(
6127 leading_sql,
6128 Dialect::Ansi,
6129 &leading_config,
6130 FixOptions {
6131 include_unsafe_fixes: true,
6132 include_rewrite_candidates: false,
6133 },
6134 )
6135 .expect("fix result");
6136 assert!(
6137 !leading_out.skipped_due_to_regression,
6138 "LT04 leading templated fix should not be treated as regression"
6139 );
6140 assert_eq!(
6141 leading_out.sql,
6142 "SELECT\n c1\n , {{ \"c2\" }} AS days_since\nFROM logs"
6143 );
6144
6145 let trailing_sql = "SELECT\n {{ \"c1\" }}\n , c2 AS days_since\nFROM logs";
6146 let trailing_config =
6147 lint_config_keep_only_rule(issue_codes::LINT_LT_004, default_lint_config());
6148 let trailing_out = apply_lint_fixes_with_options(
6149 trailing_sql,
6150 Dialect::Ansi,
6151 &trailing_config,
6152 FixOptions {
6153 include_unsafe_fixes: true,
6154 include_rewrite_candidates: false,
6155 },
6156 )
6157 .expect("fix result");
6158 assert!(
6159 !trailing_out.skipped_due_to_regression,
6160 "LT04 trailing templated fix should not be treated as regression"
6161 );
6162 assert_eq!(
6163 trailing_out.sql,
6164 "SELECT\n {{ \"c1\" }},\n c2 AS days_since\nFROM logs"
6165 );
6166 }
6167 #[test]
6168 fn rf004_core_autofix_respects_rule_filter() {
6169 let sql = "select a from users as select\n";
6170
6171 let out_rf_disabled = apply_lint_fixes(
6172 sql,
6173 Dialect::Generic,
6174 &[issue_codes::LINT_RF_004.to_string()],
6175 )
6176 .expect("fix result");
6177 assert_eq!(
6178 out_rf_disabled.sql, sql,
6179 "excluding RF_004 should block alias-keyword core autofix"
6180 );
6181
6182 let out_al_disabled = apply_lint_fixes(
6183 sql,
6184 Dialect::Generic,
6185 &[issue_codes::LINT_AL_005.to_string()],
6186 )
6187 .expect("fix result");
6188 assert!(
6189 out_al_disabled.sql.contains("alias_select"),
6190 "excluding AL_005 must not block RF_004 core autofix: {}",
6191 out_al_disabled.sql
6192 );
6193 }
6194
6195 #[test]
6196 fn rf003_core_autofix_respects_rule_filter() {
6197 let sql = "select a.id, id2 from a\n";
6198
6199 let rf_disabled_config = LintConfig {
6200 enabled: true,
6201 disabled_rules: vec![issue_codes::LINT_RF_003.to_string()],
6202 rule_configs: std::collections::BTreeMap::new(),
6203 };
6204 let out_rf_disabled = apply_lint_fixes_with_options(
6205 sql,
6206 Dialect::Generic,
6207 &rf_disabled_config,
6208 FixOptions {
6209 include_unsafe_fixes: true,
6210 include_rewrite_candidates: false,
6211 },
6212 )
6213 .expect("fix result");
6214 assert!(
6215 !out_rf_disabled.sql.contains("a.id2"),
6216 "excluding RF_003 should block reference qualification core autofix: {}",
6217 out_rf_disabled.sql
6218 );
6219
6220 let al_disabled_config = LintConfig {
6221 enabled: true,
6222 disabled_rules: vec![issue_codes::LINT_AL_005.to_string()],
6223 rule_configs: std::collections::BTreeMap::new(),
6224 };
6225 let out_al_disabled = apply_lint_fixes_with_options(
6226 sql,
6227 Dialect::Generic,
6228 &al_disabled_config,
6229 FixOptions {
6230 include_unsafe_fixes: true,
6231 include_rewrite_candidates: false,
6232 },
6233 )
6234 .expect("fix result");
6235 assert!(
6236 out_al_disabled.sql.contains("a.id2"),
6237 "excluding AL_005 must not block RF_003 core autofix: {}",
6238 out_al_disabled.sql
6239 );
6240 }
6241
6242 #[test]
6243 fn al001_fix_still_improves_with_fix_mode() {
6244 let sql = "SELECT * FROM a x JOIN b y ON x.id = y.id";
6245 assert_rule_case(sql, issue_codes::LINT_AL_001, 2, 0, 2);
6246
6247 let out = apply_lint_fixes(sql, Dialect::Generic, &[]).expect("fix result");
6248 let upper = out.sql.to_ascii_uppercase();
6249 assert!(
6250 upper.contains("FROM A AS X"),
6251 "expected explicit alias in fixed SQL, got: {}",
6252 out.sql
6253 );
6254 assert!(
6255 upper.contains("JOIN B AS Y"),
6256 "expected explicit alias in fixed SQL, got: {}",
6257 out.sql
6258 );
6259 }
6260
6261 #[test]
6262 fn al001_fix_does_not_synthesize_missing_aliases() {
6263 let sql = "SELECT COUNT(1) FROM users";
6264 let out = apply_lint_fixes(sql, Dialect::Generic, &[]).expect("fix result");
6265
6266 assert!(
6267 out.sql.to_ascii_uppercase().contains("COUNT(*)"),
6268 "expected non-AL001 fix to apply: {}",
6269 out.sql
6270 );
6271 assert!(
6272 !out.sql.to_ascii_uppercase().contains(" AS T"),
6273 "AL001 fixer must not generate synthetic aliases: {}",
6274 out.sql
6275 );
6276 }
6277
6278 #[test]
6279 fn al001_disabled_preserves_implicit_aliases_when_other_rules_fix() {
6280 let sql = "select count(1) from a x join b y on x.id = y.id";
6281 let out = apply_lint_fixes(
6282 sql,
6283 Dialect::Generic,
6284 &[issue_codes::LINT_AL_001.to_string()],
6285 )
6286 .expect("fix result");
6287
6288 assert!(
6289 out.sql.to_ascii_uppercase().contains("COUNT(*)"),
6290 "expected non-AL001 fix to apply: {}",
6291 out.sql
6292 );
6293 assert!(
6294 out.sql.to_ascii_uppercase().contains("FROM A X"),
6295 "implicit alias should be preserved when AL001 is disabled: {}",
6296 out.sql
6297 );
6298 assert!(
6299 out.sql.to_ascii_uppercase().contains("JOIN B Y"),
6300 "implicit alias should be preserved when AL001 is disabled: {}",
6301 out.sql
6302 );
6303 assert!(
6304 lint_rule_count(&out.sql, issue_codes::LINT_AL_001) > 0,
6305 "AL001 violations should remain when the rule is disabled: {}",
6306 out.sql
6307 );
6308 }
6309
6310 #[test]
6311 fn al001_implicit_config_rewrites_explicit_aliases() {
6312 let lint_config = LintConfig {
6313 enabled: true,
6314 disabled_rules: vec![],
6315 rule_configs: std::collections::BTreeMap::from([(
6316 issue_codes::LINT_AL_001.to_string(),
6317 serde_json::json!({"aliasing": "implicit"}),
6318 )]),
6319 };
6320
6321 let sql = "SELECT COUNT(1) FROM a AS x JOIN b AS y ON x.id = y.id";
6322 assert_eq!(
6323 lint_rule_count_with_config(sql, issue_codes::LINT_AL_001, &lint_config),
6324 2,
6325 "explicit aliases should violate AL001 under implicit mode"
6326 );
6327
6328 let out = apply_fix_with_config(sql, &lint_config);
6329 assert!(
6330 out.sql.to_ascii_uppercase().contains("COUNT(*)"),
6331 "expected non-AL001 fix to apply: {}",
6332 out.sql
6333 );
6334 assert!(
6335 !out.sql.to_ascii_uppercase().contains(" AS X"),
6336 "implicit-mode AL001 should remove explicit aliases: {}",
6337 out.sql
6338 );
6339 assert!(
6340 !out.sql.to_ascii_uppercase().contains(" AS Y"),
6341 "implicit-mode AL001 should remove explicit aliases: {}",
6342 out.sql
6343 );
6344 assert_eq!(
6345 lint_rule_count_with_config(&out.sql, issue_codes::LINT_AL_001, &lint_config),
6346 0,
6347 "AL001 should be resolved under implicit mode: {}",
6348 out.sql
6349 );
6350 }
6351
6352 #[test]
6353 fn table_alias_occurrences_handles_with_insert_select_aliases() {
6354 let sql = r#"
6355WITH params AS (
6356 SELECT now() - interval '1 day' AS period_start, now() AS period_end
6357),
6358overall AS (
6359 SELECT route, nav_type, mark FROM metrics.page_performance
6360),
6361device_breakdown AS (
6362 SELECT route, nav_type, mark FROM (
6363 SELECT route, nav_type, mark FROM metrics.page_performance
6364 ) sub
6365),
6366network_breakdown AS (
6367 SELECT route, nav_type, mark FROM (
6368 SELECT route, nav_type, mark FROM metrics.page_performance
6369 ) sub
6370),
6371version_breakdown AS (
6372 SELECT route, nav_type, mark FROM (
6373 SELECT route, nav_type, mark FROM metrics.page_performance
6374 ) sub
6375)
6376INSERT INTO metrics.page_performance_summary (route, period_start, period_end, nav_type, mark)
6377SELECT o.route, p.period_start, p.period_end, o.nav_type, o.mark
6378FROM overall o
6379CROSS JOIN params p
6380LEFT JOIN device_breakdown d ON d.route = o.route
6381LEFT JOIN network_breakdown n ON n.route = o.route
6382LEFT JOIN version_breakdown v ON v.route = o.route
6383ON CONFLICT (route, period_start, nav_type, mark) DO UPDATE SET
6384 period_end = EXCLUDED.period_end;
6385"#;
6386
6387 let occurrences = table_alias_occurrences(sql, Dialect::Postgres)
6388 .expect("alias occurrences should parse");
6389 let implicit_count = occurrences
6390 .iter()
6391 .filter(|alias| !alias.explicit_as)
6392 .count();
6393 assert!(
6394 implicit_count >= 8,
6395 "expected implicit aliases in CTE+INSERT query, got {}: {:?}",
6396 implicit_count,
6397 occurrences
6398 .iter()
6399 .map(|alias| (&alias.alias_key, alias.explicit_as))
6400 .collect::<Vec<_>>()
6401 );
6402 }
6403
6404 #[test]
6405 fn excluded_rule_is_not_rewritten_when_other_rules_are_fixed() {
6406 let sql = "SELECT COUNT(1) FROM t WHERE a<>b";
6407 let disabled = vec![issue_codes::LINT_CV_001.to_string()];
6408 let out = apply_lint_fixes(sql, Dialect::Generic, &disabled).expect("fix result");
6409 assert!(
6410 out.sql.to_ascii_uppercase().contains("COUNT(*)"),
6411 "expected COUNT style fix: {}",
6412 out.sql
6413 );
6414 assert!(
6415 out.sql.contains("<>"),
6416 "excluded CV_005 should remain '<>' (not '!='): {}",
6417 out.sql
6418 );
6419 assert!(
6420 !out.sql.contains("!="),
6421 "excluded CV_005 should not be rewritten to '!=': {}",
6422 out.sql
6423 );
6424 }
6425
6426 #[test]
6427 fn references_quoting_fix_keeps_reserved_identifier_quotes() {
6428 let sql = "SELECT \"FROM\" FROM t UNION SELECT 2";
6429 let out = apply_lint_fixes(sql, Dialect::Generic, &[]).expect("fix result");
6430 assert!(
6431 out.sql.contains("\"FROM\""),
6432 "reserved identifier must remain quoted: {}",
6433 out.sql
6434 );
6435 }
6436
6437 #[test]
6438 fn references_quoting_fix_unquotes_case_insensitive_dialect() {
6439 let sql = "SELECT \"CamelCase\" FROM t UNION SELECT 2";
6442 let out = apply_lint_fixes(
6443 sql,
6444 Dialect::Generic,
6445 &[issue_codes::LINT_LT_011.to_string()],
6446 )
6447 .expect("fix result");
6448 assert!(
6449 out.sql.contains("CamelCase") && !out.sql.contains("\"CamelCase\""),
6450 "case-insensitive dialect should unquote: {}",
6451 out.sql
6452 );
6453 assert!(
6454 out.sql.to_ascii_uppercase().contains("DISTINCT SELECT"),
6455 "expected another fix to persist output: {}",
6456 out.sql
6457 );
6458 }
6459
6460 #[test]
6461 fn references_quoting_fix_keeps_case_sensitive_identifier_quotes() {
6462 let sql = "SELECT \"CamelCase\" FROM t UNION SELECT 2";
6465 let out = apply_lint_fixes(
6466 sql,
6467 Dialect::Postgres,
6468 &[issue_codes::LINT_LT_011.to_string()],
6469 )
6470 .expect("fix result");
6471 assert!(
6472 out.sql.contains("\"CamelCase\""),
6473 "case-sensitive identifier must remain quoted: {}",
6474 out.sql
6475 );
6476 }
6477
6478 #[test]
6479 fn sqlfluff_fix_rule_smoke_cases_reduce_target_violations() {
6480 let cases = vec![
6481 (
6482 issue_codes::LINT_AL_001,
6483 "SELECT * FROM a x JOIN b y ON x.id = y.id",
6484 ),
6485 (
6486 issue_codes::LINT_AL_005,
6487 "SELECT u.name FROM users u JOIN orders o ON users.id = orders.user_id",
6488 ),
6489 (issue_codes::LINT_AL_009, "SELECT a AS a FROM t"),
6490 (issue_codes::LINT_AM_002, "SELECT 1 UNION SELECT 2"),
6491 (
6492 issue_codes::LINT_AM_003,
6493 "SELECT * FROM t ORDER BY a, b DESC",
6494 ),
6495 (
6496 issue_codes::LINT_AM_005,
6497 "SELECT * FROM a JOIN b ON a.id = b.id",
6498 ),
6499 (
6500 issue_codes::LINT_AM_008,
6501 "SELECT foo.a, bar.b FROM foo INNER JOIN bar",
6502 ),
6503 (issue_codes::LINT_CP_001, "SELECT a from t"),
6504 (issue_codes::LINT_CP_002, "SELECT Col, col FROM t"),
6505 (issue_codes::LINT_CP_003, "SELECT COUNT(*), count(name) FROM t"),
6506 (issue_codes::LINT_CP_004, "SELECT NULL, true FROM t"),
6507 (
6508 issue_codes::LINT_CP_005,
6509 "CREATE TABLE t (a INT, b varchar(10))",
6510 ),
6511 (
6512 issue_codes::LINT_CV_001,
6513 "SELECT * FROM t WHERE a <> b AND c != d",
6514 ),
6515 (
6516 issue_codes::LINT_CV_002,
6517 "SELECT IFNULL(x, 'default') FROM t",
6518 ),
6519 (issue_codes::LINT_CV_003, "SELECT a, FROM t"),
6520 (issue_codes::LINT_CV_004, "SELECT COUNT(1) FROM t"),
6521 (issue_codes::LINT_CV_005, "SELECT * FROM t WHERE a = NULL"),
6522 (issue_codes::LINT_CV_006, "SELECT 1 ;"),
6523 (issue_codes::LINT_CV_007, "(SELECT 1)"),
6524 (
6525 issue_codes::LINT_CV_012,
6526 "SELECT a.x, b.y FROM a JOIN b WHERE a.id = b.id",
6527 ),
6528 (issue_codes::LINT_JJ_001, "SELECT '{{foo}}' AS templated"),
6529 (issue_codes::LINT_LT_001, "SELECT payload->>'id' FROM t"),
6530 (issue_codes::LINT_LT_002, "SELECT a\n , b\nFROM t"),
6531 (issue_codes::LINT_LT_003, "SELECT a +\n b FROM t"),
6532 (issue_codes::LINT_LT_004, "SELECT a,b FROM t"),
6533 (issue_codes::LINT_LT_006, "SELECT COUNT (1) FROM t"),
6534 (
6535 issue_codes::LINT_LT_007,
6536 "WITH cte AS (\n SELECT 1) SELECT * FROM cte",
6537 ),
6538 (issue_codes::LINT_LT_009, "SELECT a,b,c,d,e FROM t"),
6539 (issue_codes::LINT_LT_010, "SELECT\nDISTINCT a\nFROM t"),
6540 (
6541 issue_codes::LINT_LT_011,
6542 "SELECT 1 UNION SELECT 2\nUNION SELECT 3",
6543 ),
6544 (issue_codes::LINT_LT_012, "SELECT 1\nFROM t"),
6545 (issue_codes::LINT_LT_013, "\n\nSELECT 1"),
6546 (issue_codes::LINT_LT_014, "SELECT a FROM t\nWHERE a=1"),
6547 (issue_codes::LINT_LT_015, "SELECT 1\n\n\nFROM t"),
6548 (issue_codes::LINT_RF_003, "SELECT a.id, id2 FROM a"),
6549 (issue_codes::LINT_RF_006, "SELECT \"good_name\" FROM t"),
6550 (
6551 issue_codes::LINT_ST_001,
6552 "SELECT CASE WHEN x > 1 THEN 'a' ELSE NULL END FROM t",
6553 ),
6554 (
6555 issue_codes::LINT_ST_004,
6556 "SELECT CASE WHEN species = 'Rat' THEN 'Squeak' ELSE CASE WHEN species = 'Dog' THEN 'Woof' END END FROM mytable",
6557 ),
6558 (
6559 issue_codes::LINT_ST_002,
6560 "SELECT CASE WHEN x > 0 THEN true ELSE false END FROM t",
6561 ),
6562 (
6563 issue_codes::LINT_ST_005,
6564 "SELECT * FROM t JOIN (SELECT * FROM u) sub ON t.id = sub.id",
6565 ),
6566 (issue_codes::LINT_ST_006, "SELECT a + 1, a FROM t"),
6567 (
6568 issue_codes::LINT_ST_007,
6569 "SELECT * FROM a JOIN b USING (id)",
6570 ),
6571 (issue_codes::LINT_ST_008, "SELECT DISTINCT(a) FROM t"),
6572 (
6573 issue_codes::LINT_ST_009,
6574 "SELECT * FROM a x JOIN b y ON y.id = x.id",
6575 ),
6576 (issue_codes::LINT_ST_012, "SELECT 1;;"),
6577 ];
6578
6579 for (code, sql) in cases {
6580 let before = lint_rule_count(sql, code);
6581 assert!(before > 0, "expected {code} to trigger before fix: {sql}");
6582 let out = apply_lint_fixes(sql, Dialect::Generic, &[]).expect("fix result");
6583 assert!(
6584 !out.skipped_due_to_comments,
6585 "test SQL should not be skipped: {sql}"
6586 );
6587 let after = lint_rule_count(&out.sql, code);
6588 assert!(
6589 after < before || out.sql != sql,
6590 "expected {code} count to decrease or SQL to be rewritten. before={before} after={after}\ninput={sql}\noutput={}",
6591 out.sql
6592 );
6593 }
6594 }
6595
6596 #[test]
6599 fn cv012_simple_where_join_to_on() {
6600 let sql = "SELECT a.x, b.y FROM a JOIN b WHERE a.id = b.id";
6601 let out = apply_lint_fixes(sql, Dialect::Generic, &[]).expect("fix");
6602 let lower = out.sql.to_ascii_lowercase();
6603 assert!(
6604 lower.contains(" on ") && !lower.contains("where"),
6605 "expected JOIN ON without WHERE: {}",
6606 out.sql
6607 );
6608 }
6609
6610 #[test]
6611 fn cv012_mixed_where_keeps_non_join_predicates() {
6612 let sql = "SELECT a.x FROM a JOIN b WHERE a.id = b.id AND a.val > 10";
6613 let out = apply_lint_fixes(sql, Dialect::Generic, &[]).expect("fix");
6614 let lower = out.sql.to_ascii_lowercase();
6615 assert!(lower.contains(" on "), "expected JOIN ON: {}", out.sql);
6616 assert!(
6617 lower.contains("where"),
6618 "expected remaining WHERE: {}",
6619 out.sql
6620 );
6621 }
6622
6623 #[test]
6624 fn cv012_multi_join_chain() {
6625 let sql = "SELECT * FROM a JOIN b JOIN c WHERE a.id = b.id AND b.id = c.id";
6626 let out = apply_lint_fixes(
6627 sql,
6628 Dialect::Generic,
6629 &[issue_codes::LINT_AM_005.to_string()],
6630 )
6631 .expect("fix");
6632 let lower = out.sql.to_ascii_lowercase();
6633 let on_count = lower.matches(" on ").count();
6635 assert!(on_count >= 2, "expected at least 2 ON clauses: {}", out.sql);
6636 assert!(
6637 !lower.contains("where"),
6638 "all predicates should be extracted: {}",
6639 out.sql
6640 );
6641 }
6642
6643 #[test]
6644 fn cv012_preserves_explicit_on() {
6645 let sql = "SELECT * FROM a JOIN b ON a.id = b.id";
6646 let out = apply_lint_fixes(sql, Dialect::Generic, &[]).expect("fix");
6647 assert_eq!(
6648 lint_rule_count(sql, issue_codes::LINT_CV_012),
6649 0,
6650 "explicit ON should not trigger CV_012"
6651 );
6652 let lower = out.sql.to_ascii_lowercase();
6653 assert!(
6654 lower.contains("on a.id = b.id"),
6655 "ON clause should be preserved: {}",
6656 out.sql
6657 );
6658 }
6659
6660 #[test]
6661 fn cv012_idempotent() {
6662 let sql = "SELECT a.x, b.y FROM a JOIN b WHERE a.id = b.id";
6663 let lint_config = LintConfig {
6664 enabled: true,
6665 disabled_rules: vec![issue_codes::LINT_LT_014.to_string()],
6666 rule_configs: std::collections::BTreeMap::new(),
6667 };
6668 let out1 = apply_lint_fixes_with_options(
6669 sql,
6670 Dialect::Generic,
6671 &lint_config,
6672 FixOptions {
6673 include_unsafe_fixes: true,
6674 include_rewrite_candidates: false,
6675 },
6676 )
6677 .expect("fix");
6678 let out2 = apply_lint_fixes_with_options(
6679 &out1.sql,
6680 Dialect::Generic,
6681 &lint_config,
6682 FixOptions {
6683 include_unsafe_fixes: true,
6684 include_rewrite_candidates: false,
6685 },
6686 )
6687 .expect("fix2");
6688 assert_eq!(
6689 out1.sql.trim_end(),
6690 out2.sql.trim_end(),
6691 "second pass should be idempotent aside from trailing-whitespace normalization"
6692 );
6693 }
6694}