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(CreateView { query, .. }) => {
2643 collect_table_alias_idents_in_query(query, visitor)
2644 }
2645 Statement::CreateTable(create) => {
2646 if let Some(query) = &create.query {
2647 collect_table_alias_idents_in_query(query, visitor);
2648 }
2649 }
2650 Statement::Merge(Merge { table, source, .. }) => {
2651 collect_table_alias_idents_in_table_factor(table, visitor);
2652 collect_table_alias_idents_in_table_factor(source, visitor);
2653 }
2654 _ => {}
2655 }
2656}
2657
2658fn collect_table_alias_idents_in_query<F: FnMut(&Ident)>(query: &Query, visitor: &mut F) {
2659 if let Some(with) = &query.with {
2660 for cte in &with.cte_tables {
2661 collect_table_alias_idents_in_query(&cte.query, visitor);
2662 }
2663 }
2664
2665 collect_table_alias_idents_in_set_expr(&query.body, visitor);
2666}
2667
2668fn collect_table_alias_idents_in_set_expr<F: FnMut(&Ident)>(set_expr: &SetExpr, visitor: &mut F) {
2669 match set_expr {
2670 SetExpr::Select(select) => {
2671 for table in &select.from {
2672 collect_table_alias_idents_in_table_with_joins(table, visitor);
2673 }
2674 }
2675 SetExpr::Query(query) => collect_table_alias_idents_in_query(query, visitor),
2676 SetExpr::SetOperation { left, right, .. } => {
2677 collect_table_alias_idents_in_set_expr(left, visitor);
2678 collect_table_alias_idents_in_set_expr(right, visitor);
2679 }
2680 SetExpr::Insert(statement)
2681 | SetExpr::Update(statement)
2682 | SetExpr::Delete(statement)
2683 | SetExpr::Merge(statement) => collect_table_alias_idents_in_statement(statement, visitor),
2684 _ => {}
2685 }
2686}
2687
2688fn collect_table_alias_idents_in_table_with_joins<F: FnMut(&Ident)>(
2689 table_with_joins: &TableWithJoins,
2690 visitor: &mut F,
2691) {
2692 collect_table_alias_idents_in_table_factor(&table_with_joins.relation, visitor);
2693 for join in &table_with_joins.joins {
2694 collect_table_alias_idents_in_table_factor(&join.relation, visitor);
2695 }
2696}
2697
2698fn collect_table_alias_idents_in_table_factor<F: FnMut(&Ident)>(
2699 table_factor: &TableFactor,
2700 visitor: &mut F,
2701) {
2702 if let Some(alias) = table_factor_alias_ident(table_factor) {
2703 visitor(alias);
2704 }
2705
2706 match table_factor {
2707 TableFactor::Derived { subquery, .. } => {
2708 collect_table_alias_idents_in_query(subquery, visitor)
2709 }
2710 TableFactor::NestedJoin {
2711 table_with_joins, ..
2712 } => collect_table_alias_idents_in_table_with_joins(table_with_joins, visitor),
2713 TableFactor::Pivot { table, .. }
2714 | TableFactor::Unpivot { table, .. }
2715 | TableFactor::MatchRecognize { table, .. } => {
2716 collect_table_alias_idents_in_table_factor(table, visitor)
2717 }
2718 _ => {}
2719 }
2720}
2721
2722#[cfg(test)]
2723fn is_ascii_whitespace_byte(byte: u8) -> bool {
2724 matches!(byte, b' ' | b'\n' | b'\r' | b'\t' | 0x0b | 0x0c)
2725}
2726
2727#[cfg(test)]
2728fn is_ascii_ident_start(byte: u8) -> bool {
2729 byte.is_ascii_alphabetic() || byte == b'_'
2730}
2731
2732#[cfg(test)]
2733fn is_ascii_ident_continue(byte: u8) -> bool {
2734 byte.is_ascii_alphanumeric() || byte == b'_'
2735}
2736
2737#[cfg(test)]
2738fn skip_ascii_whitespace(bytes: &[u8], mut idx: usize) -> usize {
2739 while idx < bytes.len() && is_ascii_whitespace_byte(bytes[idx]) {
2740 idx += 1;
2741 }
2742 idx
2743}
2744
2745#[cfg(test)]
2746fn consume_ascii_identifier(bytes: &[u8], start: usize) -> Option<usize> {
2747 if start >= bytes.len() || !is_ascii_ident_start(bytes[start]) {
2748 return None;
2749 }
2750 let mut idx = start + 1;
2751 while idx < bytes.len() && is_ascii_ident_continue(bytes[idx]) {
2752 idx += 1;
2753 }
2754 Some(idx)
2755}
2756
2757#[cfg(test)]
2758fn is_word_boundary_for_keyword(bytes: &[u8], idx: usize) -> bool {
2759 idx == 0 || idx >= bytes.len() || !is_ascii_ident_continue(bytes[idx])
2760}
2761
2762#[cfg(test)]
2763fn match_ascii_keyword_at(bytes: &[u8], start: usize, keyword_upper: &[u8]) -> Option<usize> {
2764 let end = start.checked_add(keyword_upper.len())?;
2765 if end > bytes.len() {
2766 return None;
2767 }
2768 if !is_word_boundary_for_keyword(bytes, start.saturating_sub(1))
2769 || !is_word_boundary_for_keyword(bytes, end)
2770 {
2771 return None;
2772 }
2773 let matches = bytes[start..end]
2774 .iter()
2775 .zip(keyword_upper.iter())
2776 .all(|(actual, expected)| actual.to_ascii_uppercase() == *expected);
2777 if matches {
2778 Some(end)
2779 } else {
2780 None
2781 }
2782}
2783
2784#[cfg(test)]
2785fn parse_subquery_alias_suffix(suffix: &str) -> Option<String> {
2786 let bytes = suffix.as_bytes();
2787 let mut i = skip_ascii_whitespace(bytes, 0);
2788 if let Some(as_end) = match_ascii_keyword_at(bytes, i, b"AS") {
2789 let after_as = skip_ascii_whitespace(bytes, as_end);
2790 if after_as == as_end {
2791 return None;
2792 }
2793 i = after_as;
2794 }
2795
2796 let alias_start = i;
2797 let alias_end = consume_ascii_identifier(bytes, alias_start)?;
2798 i = skip_ascii_whitespace(bytes, alias_end);
2799 if i < bytes.len() && bytes[i] == b';' {
2800 i += 1;
2801 i = skip_ascii_whitespace(bytes, i);
2802 }
2803 if i != bytes.len() {
2804 return None;
2805 }
2806 Some(suffix[alias_start..alias_end].to_string())
2807}
2808
2809#[cfg(test)]
2810fn fix_subquery_to_cte(sql: &str) -> String {
2811 let bytes = sql.as_bytes();
2812 let mut i = skip_ascii_whitespace(bytes, 0);
2813 let Some(select_end) = match_ascii_keyword_at(bytes, i, b"SELECT") else {
2814 return sql.to_string();
2815 };
2816 i = skip_ascii_whitespace(bytes, select_end);
2817 if i == select_end || i >= bytes.len() || bytes[i] != b'*' {
2818 return sql.to_string();
2819 }
2820 i += 1;
2821 let from_start = skip_ascii_whitespace(bytes, i);
2822 if from_start == i {
2823 return sql.to_string();
2824 }
2825 let Some(from_end) = match_ascii_keyword_at(bytes, from_start, b"FROM") else {
2826 return sql.to_string();
2827 };
2828 let open_paren_idx = skip_ascii_whitespace(bytes, from_end);
2829 if open_paren_idx == from_end || open_paren_idx >= bytes.len() || bytes[open_paren_idx] != b'('
2830 {
2831 return sql.to_string();
2832 };
2833
2834 let Some(close_paren_idx) = find_matching_parenthesis_outside_quotes(sql, open_paren_idx)
2835 else {
2836 return sql.to_string();
2837 };
2838
2839 let subquery = sql[open_paren_idx + 1..close_paren_idx].trim();
2840 if !subquery.to_ascii_lowercase().starts_with("select") {
2841 return sql.to_string();
2842 }
2843
2844 let suffix = &sql[close_paren_idx + 1..];
2845 let Some(alias) = parse_subquery_alias_suffix(suffix) else {
2846 return sql.to_string();
2847 };
2848
2849 let mut rewritten = format!("WITH {alias} AS ({subquery}) SELECT * FROM {alias}");
2850 if suffix.trim_end().ends_with(';') {
2851 rewritten.push(';');
2852 }
2853 rewritten
2854}
2855
2856#[cfg(test)]
2857fn find_matching_parenthesis_outside_quotes(sql: &str, open_paren_idx: usize) -> Option<usize> {
2858 #[derive(Clone, Copy, PartialEq, Eq)]
2859 enum Mode {
2860 Outside,
2861 SingleQuote,
2862 DoubleQuote,
2863 BacktickQuote,
2864 BracketQuote,
2865 }
2866
2867 let bytes = sql.as_bytes();
2868 if open_paren_idx >= bytes.len() || bytes[open_paren_idx] != b'(' {
2869 return None;
2870 }
2871
2872 let mut depth = 0usize;
2873 let mut mode = Mode::Outside;
2874 let mut i = open_paren_idx;
2875
2876 while i < bytes.len() {
2877 let b = bytes[i];
2878 let next = bytes.get(i + 1).copied();
2879
2880 match mode {
2881 Mode::Outside => {
2882 if b == b'\'' {
2883 mode = Mode::SingleQuote;
2884 i += 1;
2885 continue;
2886 }
2887 if b == b'"' {
2888 mode = Mode::DoubleQuote;
2889 i += 1;
2890 continue;
2891 }
2892 if b == b'`' {
2893 mode = Mode::BacktickQuote;
2894 i += 1;
2895 continue;
2896 }
2897 if b == b'[' {
2898 mode = Mode::BracketQuote;
2899 i += 1;
2900 continue;
2901 }
2902 if b == b'(' {
2903 depth += 1;
2904 i += 1;
2905 continue;
2906 }
2907 if b == b')' {
2908 depth = depth.checked_sub(1)?;
2909 if depth == 0 {
2910 return Some(i);
2911 }
2912 }
2913 i += 1;
2914 }
2915 Mode::SingleQuote => {
2916 if b == b'\'' {
2917 if next == Some(b'\'') {
2918 i += 2;
2919 } else {
2920 mode = Mode::Outside;
2921 i += 1;
2922 }
2923 } else {
2924 i += 1;
2925 }
2926 }
2927 Mode::DoubleQuote => {
2928 if b == b'"' {
2929 if next == Some(b'"') {
2930 i += 2;
2931 } else {
2932 mode = Mode::Outside;
2933 i += 1;
2934 }
2935 } else {
2936 i += 1;
2937 }
2938 }
2939 Mode::BacktickQuote => {
2940 if b == b'`' {
2941 if next == Some(b'`') {
2942 i += 2;
2943 } else {
2944 mode = Mode::Outside;
2945 i += 1;
2946 }
2947 } else {
2948 i += 1;
2949 }
2950 }
2951 Mode::BracketQuote => {
2952 if b == b']' {
2953 if next == Some(b']') {
2954 i += 2;
2955 } else {
2956 mode = Mode::Outside;
2957 i += 1;
2958 }
2959 } else {
2960 i += 1;
2961 }
2962 }
2963 }
2964 }
2965
2966 None
2967}
2968
2969fn fix_statement(stmt: &mut Statement, rule_filter: &RuleFilter) {
2970 match stmt {
2971 Statement::Query(query) => fix_query(query, rule_filter),
2972 Statement::Insert(insert) => {
2973 if let Some(source) = insert.source.as_mut() {
2974 fix_query(source, rule_filter);
2975 }
2976 }
2977 Statement::CreateView(CreateView { query, .. }) => fix_query(query, rule_filter),
2978 Statement::CreateTable(create) => {
2979 if let Some(query) = create.query.as_mut() {
2980 fix_query(query, rule_filter);
2981 }
2982 }
2983 _ => {}
2984 }
2985}
2986
2987fn fix_query(query: &mut Query, rule_filter: &RuleFilter) {
2988 if let Some(with) = query.with.as_mut() {
2989 for cte in &mut with.cte_tables {
2990 fix_query(&mut cte.query, rule_filter);
2991 }
2992 }
2993
2994 fix_set_expr(query.body.as_mut(), rule_filter);
2995 rewrite_simple_derived_subqueries_to_ctes(query, rule_filter);
2996
2997 if let Some(order_by) = query.order_by.as_mut() {
2998 fix_order_by(order_by, rule_filter);
2999 }
3000
3001 if let Some(limit_clause) = query.limit_clause.as_mut() {
3002 fix_limit_clause(limit_clause, rule_filter);
3003 }
3004
3005 if let Some(fetch) = query.fetch.as_mut() {
3006 if let Some(quantity) = fetch.quantity.as_mut() {
3007 fix_expr(quantity, rule_filter);
3008 }
3009 }
3010}
3011
3012fn fix_set_expr(body: &mut SetExpr, rule_filter: &RuleFilter) {
3013 match body {
3014 SetExpr::Select(select) => fix_select(select, rule_filter),
3015 SetExpr::Query(query) => fix_query(query, rule_filter),
3016 SetExpr::SetOperation { left, right, .. } => {
3017 fix_set_expr(left, rule_filter);
3018 fix_set_expr(right, rule_filter);
3019 }
3020 SetExpr::Values(values) => {
3021 for row in &mut values.rows {
3022 for expr in row {
3023 fix_expr(expr, rule_filter);
3024 }
3025 }
3026 }
3027 SetExpr::Insert(stmt)
3028 | SetExpr::Update(stmt)
3029 | SetExpr::Delete(stmt)
3030 | SetExpr::Merge(stmt) => fix_statement(stmt, rule_filter),
3031 _ => {}
3032 }
3033}
3034
3035fn fix_select(select: &mut Select, rule_filter: &RuleFilter) {
3036 for item in &mut select.projection {
3037 match item {
3038 SelectItem::UnnamedExpr(expr) => {
3039 fix_expr(expr, rule_filter);
3040 }
3041 SelectItem::ExprWithAlias { expr, .. } => {
3042 fix_expr(expr, rule_filter);
3043 }
3044 SelectItem::QualifiedWildcard(SelectItemQualifiedWildcardKind::Expr(expr), _) => {
3045 fix_expr(expr, rule_filter);
3046 }
3047 _ => {}
3048 }
3049 }
3050
3051 for table_with_joins in &mut select.from {
3052 if rule_filter.allows(issue_codes::LINT_CV_008) {
3053 rewrite_right_join_to_left(table_with_joins);
3054 }
3055
3056 fix_table_factor(&mut table_with_joins.relation, rule_filter);
3057
3058 let mut left_ref = table_factor_reference_name(&table_with_joins.relation);
3059
3060 for join in &mut table_with_joins.joins {
3061 let right_ref = table_factor_reference_name(&join.relation);
3062 if rule_filter.allows(issue_codes::LINT_ST_007) {
3063 rewrite_using_join_constraint(
3064 &mut join.join_operator,
3065 left_ref.as_deref(),
3066 right_ref.as_deref(),
3067 );
3068 }
3069
3070 fix_table_factor(&mut join.relation, rule_filter);
3071 fix_join_operator(&mut join.join_operator, rule_filter);
3072
3073 if right_ref.is_some() {
3074 left_ref = right_ref;
3075 }
3076 }
3077 }
3078
3079 if let Some(prewhere) = select.prewhere.as_mut() {
3080 fix_expr(prewhere, rule_filter);
3081 }
3082
3083 if let Some(selection) = select.selection.as_mut() {
3084 fix_expr(selection, rule_filter);
3085 }
3086
3087 if let Some(having) = select.having.as_mut() {
3088 fix_expr(having, rule_filter);
3089 }
3090
3091 if let Some(qualify) = select.qualify.as_mut() {
3092 fix_expr(qualify, rule_filter);
3093 }
3094
3095 if let GroupByExpr::Expressions(exprs, _) = &mut select.group_by {
3096 for expr in exprs {
3097 fix_expr(expr, rule_filter);
3098 }
3099 }
3100
3101 for expr in &mut select.cluster_by {
3102 fix_expr(expr, rule_filter);
3103 }
3104
3105 for expr in &mut select.distribute_by {
3106 fix_expr(expr, rule_filter);
3107 }
3108
3109 for expr in &mut select.sort_by {
3110 fix_expr(&mut expr.expr, rule_filter);
3111 }
3112
3113 for lateral_view in &mut select.lateral_views {
3114 fix_expr(&mut lateral_view.lateral_view, rule_filter);
3115 }
3116
3117 for connect_by_kind in &mut select.connect_by {
3118 match connect_by_kind {
3119 ConnectByKind::ConnectBy { relationships, .. } => {
3120 for relationship in relationships {
3121 fix_expr(relationship, rule_filter);
3122 }
3123 }
3124 ConnectByKind::StartWith { condition, .. } => {
3125 fix_expr(condition, rule_filter);
3126 }
3127 }
3128 }
3129}
3130
3131fn rewrite_simple_derived_subqueries_to_ctes(query: &mut Query, rule_filter: &RuleFilter) {
3132 if !rule_filter.allows(issue_codes::LINT_ST_005) {
3133 return;
3134 }
3135
3136 let SetExpr::Select(select) = query.body.as_mut() else {
3137 return;
3138 };
3139
3140 let outer_source_names = select_source_names_upper(select);
3141 let mut used_cte_names: HashSet<String> = query
3142 .with
3143 .as_ref()
3144 .map(|with| {
3145 with.cte_tables
3146 .iter()
3147 .map(|cte| cte.alias.name.value.to_ascii_uppercase())
3148 .collect()
3149 })
3150 .unwrap_or_default();
3151 used_cte_names.extend(outer_source_names.iter().cloned());
3152
3153 let mut new_ctes = Vec::new();
3154
3155 for table_with_joins in &mut select.from {
3156 if rule_filter.st005_forbid_subquery_in.forbid_from() {
3157 if let Some(cte) = rewrite_derived_table_factor_to_cte(
3158 &mut table_with_joins.relation,
3159 &outer_source_names,
3160 &mut used_cte_names,
3161 ) {
3162 new_ctes.push(cte);
3163 }
3164 }
3165
3166 if rule_filter.st005_forbid_subquery_in.forbid_join() {
3167 for join in &mut table_with_joins.joins {
3168 if let Some(cte) = rewrite_derived_table_factor_to_cte(
3169 &mut join.relation,
3170 &outer_source_names,
3171 &mut used_cte_names,
3172 ) {
3173 new_ctes.push(cte);
3174 }
3175 }
3176 }
3177 }
3178
3179 if new_ctes.is_empty() {
3180 return;
3181 }
3182
3183 let with = query.with.get_or_insert_with(|| With {
3184 with_token: AttachedToken::empty(),
3185 recursive: false,
3186 cte_tables: Vec::new(),
3187 });
3188 with.cte_tables.extend(new_ctes);
3189}
3190
3191fn rewrite_derived_table_factor_to_cte(
3192 relation: &mut TableFactor,
3193 outer_source_names: &HashSet<String>,
3194 used_cte_names: &mut HashSet<String>,
3195) -> Option<Cte> {
3196 let (lateral, subquery, alias) = match relation {
3197 TableFactor::Derived {
3198 lateral,
3199 subquery,
3200 alias,
3201 ..
3202 } => (lateral, subquery, alias),
3203 _ => return None,
3204 };
3205
3206 if *lateral {
3207 return None;
3208 }
3209
3210 if !matches!(subquery.body.as_ref(), SetExpr::Select(_))
3213 || query_text_references_outer_sources(subquery, outer_source_names)
3214 {
3215 return None;
3216 }
3217
3218 let cte_alias = alias.clone().unwrap_or_else(|| TableAlias {
3219 explicit: false,
3220 name: Ident::new(next_generated_cte_name(used_cte_names)),
3221 columns: Vec::new(),
3222 });
3223 let cte_name_ident = cte_alias.name.clone();
3224 let cte_name_upper = cte_name_ident.value.to_ascii_uppercase();
3225 used_cte_names.insert(cte_name_upper);
3226
3227 let cte = Cte {
3228 alias: cte_alias,
3229 query: subquery.clone(),
3230 from: None,
3231 materialized: None,
3232 closing_paren_token: AttachedToken::empty(),
3233 };
3234
3235 *relation = TableFactor::Table {
3236 name: vec![cte_name_ident].into(),
3237 alias: None,
3238 args: None,
3239 with_hints: Vec::new(),
3240 version: None,
3241 with_ordinality: false,
3242 partitions: Vec::new(),
3243 json_path: None,
3244 sample: None,
3245 index_hints: Vec::new(),
3246 };
3247
3248 Some(cte)
3249}
3250
3251fn next_generated_cte_name(used_cte_names: &HashSet<String>) -> String {
3252 let mut index = 1usize;
3253 loop {
3254 let candidate = format!("cte_subquery_{index}");
3255 if !used_cte_names.contains(&candidate.to_ascii_uppercase()) {
3256 return candidate;
3257 }
3258 index += 1;
3259 }
3260}
3261
3262fn query_text_references_outer_sources(
3263 query: &Query,
3264 outer_source_names: &HashSet<String>,
3265) -> bool {
3266 if outer_source_names.is_empty() {
3267 return false;
3268 }
3269
3270 let rendered_upper = query.to_string().to_ascii_uppercase();
3271 outer_source_names
3272 .iter()
3273 .any(|name| rendered_upper.contains(&format!("{name}.")))
3274}
3275
3276fn select_source_names_upper(select: &Select) -> HashSet<String> {
3277 let mut names = HashSet::new();
3278 for table in &select.from {
3279 collect_source_names_from_table_factor(&table.relation, &mut names);
3280 for join in &table.joins {
3281 collect_source_names_from_table_factor(&join.relation, &mut names);
3282 }
3283 }
3284 names
3285}
3286
3287fn collect_source_names_from_table_factor(relation: &TableFactor, names: &mut HashSet<String>) {
3288 match relation {
3289 TableFactor::Table { name, alias, .. } => {
3290 if let Some(last) = name.0.last().and_then(|part| part.as_ident()) {
3291 names.insert(last.value.to_ascii_uppercase());
3292 }
3293 if let Some(alias) = alias {
3294 names.insert(alias.name.value.to_ascii_uppercase());
3295 }
3296 }
3297 TableFactor::Derived { alias, .. }
3298 | TableFactor::TableFunction { alias, .. }
3299 | TableFactor::Function { alias, .. }
3300 | TableFactor::UNNEST { alias, .. }
3301 | TableFactor::JsonTable { alias, .. }
3302 | TableFactor::OpenJsonTable { alias, .. }
3303 | TableFactor::NestedJoin { alias, .. }
3304 | TableFactor::Pivot { alias, .. }
3305 | TableFactor::Unpivot { alias, .. } => {
3306 if let Some(alias) = alias {
3307 names.insert(alias.name.value.to_ascii_uppercase());
3308 }
3309 }
3310 _ => {}
3311 }
3312}
3313
3314fn rewrite_right_join_to_left(table_with_joins: &mut TableWithJoins) {
3315 while let Some(index) = table_with_joins
3316 .joins
3317 .iter()
3318 .position(|join| rewritable_right_join(&join.join_operator))
3319 {
3320 rewrite_right_join_at_index(table_with_joins, index);
3321 }
3322}
3323
3324fn rewrite_right_join_at_index(table_with_joins: &mut TableWithJoins, index: usize) {
3325 let mut suffix = table_with_joins.joins.split_off(index);
3326 let mut join = suffix.remove(0);
3327
3328 let old_operator = std::mem::replace(
3329 &mut join.join_operator,
3330 JoinOperator::CrossJoin(JoinConstraint::None),
3331 );
3332 let Some(new_operator) = rewritten_left_join_operator(old_operator) else {
3333 table_with_joins.joins.push(join);
3334 table_with_joins.joins.append(&mut suffix);
3335 return;
3336 };
3337
3338 let previous_relation = std::mem::replace(&mut table_with_joins.relation, join.relation);
3339 let prefix_joins = std::mem::take(&mut table_with_joins.joins);
3340
3341 join.relation = if prefix_joins.is_empty() {
3342 previous_relation
3343 } else {
3344 TableFactor::NestedJoin {
3345 table_with_joins: Box::new(TableWithJoins {
3346 relation: previous_relation,
3347 joins: prefix_joins,
3348 }),
3349 alias: None,
3350 }
3351 };
3352 join.join_operator = new_operator;
3353
3354 table_with_joins.joins.push(join);
3355 table_with_joins.joins.append(&mut suffix);
3356}
3357
3358fn rewritable_right_join(operator: &JoinOperator) -> bool {
3359 matches!(
3360 operator,
3361 JoinOperator::Right(_)
3362 | JoinOperator::RightOuter(_)
3363 | JoinOperator::RightSemi(_)
3364 | JoinOperator::RightAnti(_)
3365 )
3366}
3367
3368fn rewritten_left_join_operator(operator: JoinOperator) -> Option<JoinOperator> {
3369 match operator {
3370 JoinOperator::Right(constraint) => Some(JoinOperator::Left(constraint)),
3371 JoinOperator::RightOuter(constraint) => Some(JoinOperator::LeftOuter(constraint)),
3372 JoinOperator::RightSemi(constraint) => Some(JoinOperator::LeftSemi(constraint)),
3373 JoinOperator::RightAnti(constraint) => Some(JoinOperator::LeftAnti(constraint)),
3374 _ => None,
3375 }
3376}
3377
3378fn table_factor_alias_ident(relation: &TableFactor) -> Option<&Ident> {
3379 let alias = match relation {
3380 TableFactor::Table { alias, .. }
3381 | TableFactor::Derived { alias, .. }
3382 | TableFactor::TableFunction { alias, .. }
3383 | TableFactor::Function { alias, .. }
3384 | TableFactor::UNNEST { alias, .. }
3385 | TableFactor::JsonTable { alias, .. }
3386 | TableFactor::OpenJsonTable { alias, .. }
3387 | TableFactor::NestedJoin { alias, .. }
3388 | TableFactor::Pivot { alias, .. }
3389 | TableFactor::Unpivot { alias, .. } => alias.as_ref(),
3390 _ => None,
3391 }?;
3392
3393 Some(&alias.name)
3394}
3395
3396fn table_factor_reference_name(relation: &TableFactor) -> Option<String> {
3397 match relation {
3398 TableFactor::Table { name, alias, .. } => {
3399 if let Some(alias) = alias {
3400 Some(alias.name.value.clone())
3401 } else {
3402 name.0
3403 .last()
3404 .and_then(|part| part.as_ident())
3405 .map(|ident| ident.value.clone())
3406 }
3407 }
3408 _ => None,
3409 }
3410}
3411
3412fn rewrite_using_join_constraint(
3413 join_operator: &mut JoinOperator,
3414 left_ref: Option<&str>,
3415 right_ref: Option<&str>,
3416) {
3417 let (Some(left_ref), Some(right_ref)) = (left_ref, right_ref) else {
3418 return;
3419 };
3420
3421 let Some(constraint) = join_constraint_mut(join_operator) else {
3422 return;
3423 };
3424
3425 let JoinConstraint::Using(columns) = constraint else {
3426 return;
3427 };
3428
3429 if columns.is_empty() {
3430 return;
3431 }
3432
3433 let mut combined: Option<Expr> = None;
3434 for object_name in columns.iter() {
3435 let Some(column_ident) = object_name
3436 .0
3437 .last()
3438 .and_then(|part| part.as_ident())
3439 .cloned()
3440 else {
3441 continue;
3442 };
3443
3444 let equality = Expr::BinaryOp {
3445 left: Box::new(Expr::CompoundIdentifier(vec![
3446 Ident::new(left_ref),
3447 column_ident.clone(),
3448 ])),
3449 op: BinaryOperator::Eq,
3450 right: Box::new(Expr::CompoundIdentifier(vec![
3451 Ident::new(right_ref),
3452 column_ident,
3453 ])),
3454 };
3455
3456 combined = Some(match combined {
3457 Some(prev) => Expr::BinaryOp {
3458 left: Box::new(prev),
3459 op: BinaryOperator::And,
3460 right: Box::new(equality),
3461 },
3462 None => equality,
3463 });
3464 }
3465
3466 if let Some(on_expr) = combined {
3467 *constraint = JoinConstraint::On(on_expr);
3468 }
3469}
3470
3471fn fix_table_factor(relation: &mut TableFactor, rule_filter: &RuleFilter) {
3472 match relation {
3473 TableFactor::Table {
3474 args, with_hints, ..
3475 } => {
3476 if let Some(args) = args {
3477 for arg in &mut args.args {
3478 fix_function_arg(arg, rule_filter);
3479 }
3480 }
3481 for hint in with_hints {
3482 fix_expr(hint, rule_filter);
3483 }
3484 }
3485 TableFactor::Derived { subquery, .. } => fix_query(subquery, rule_filter),
3486 TableFactor::TableFunction { expr, .. } => fix_expr(expr, rule_filter),
3487 TableFactor::Function { args, .. } => {
3488 for arg in args {
3489 fix_function_arg(arg, rule_filter);
3490 }
3491 }
3492 TableFactor::UNNEST { array_exprs, .. } => {
3493 for expr in array_exprs {
3494 fix_expr(expr, rule_filter);
3495 }
3496 }
3497 TableFactor::NestedJoin {
3498 table_with_joins, ..
3499 } => {
3500 if rule_filter.allows(issue_codes::LINT_CV_008) {
3501 rewrite_right_join_to_left(table_with_joins);
3502 }
3503
3504 fix_table_factor(&mut table_with_joins.relation, rule_filter);
3505
3506 let mut left_ref = table_factor_reference_name(&table_with_joins.relation);
3507
3508 for join in &mut table_with_joins.joins {
3509 let right_ref = table_factor_reference_name(&join.relation);
3510 if rule_filter.allows(issue_codes::LINT_ST_007) {
3511 rewrite_using_join_constraint(
3512 &mut join.join_operator,
3513 left_ref.as_deref(),
3514 right_ref.as_deref(),
3515 );
3516 }
3517
3518 fix_table_factor(&mut join.relation, rule_filter);
3519 fix_join_operator(&mut join.join_operator, rule_filter);
3520
3521 if right_ref.is_some() {
3522 left_ref = right_ref;
3523 }
3524 }
3525 }
3526 TableFactor::Pivot {
3527 table,
3528 aggregate_functions,
3529 value_column,
3530 default_on_null,
3531 ..
3532 } => {
3533 fix_table_factor(table, rule_filter);
3534 for func in aggregate_functions {
3535 fix_expr(&mut func.expr, rule_filter);
3536 }
3537 for expr in value_column {
3538 fix_expr(expr, rule_filter);
3539 }
3540 if let Some(expr) = default_on_null {
3541 fix_expr(expr, rule_filter);
3542 }
3543 }
3544 TableFactor::Unpivot {
3545 table,
3546 value,
3547 columns,
3548 ..
3549 } => {
3550 fix_table_factor(table, rule_filter);
3551 fix_expr(value, rule_filter);
3552 for column in columns {
3553 fix_expr(&mut column.expr, rule_filter);
3554 }
3555 }
3556 TableFactor::JsonTable { json_expr, .. } => fix_expr(json_expr, rule_filter),
3557 TableFactor::OpenJsonTable { json_expr, .. } => fix_expr(json_expr, rule_filter),
3558 _ => {}
3559 }
3560}
3561
3562fn fix_join_operator(op: &mut JoinOperator, rule_filter: &RuleFilter) {
3563 match op {
3564 JoinOperator::Join(constraint)
3565 | JoinOperator::Inner(constraint)
3566 | JoinOperator::Left(constraint)
3567 | JoinOperator::LeftOuter(constraint)
3568 | JoinOperator::Right(constraint)
3569 | JoinOperator::RightOuter(constraint)
3570 | JoinOperator::FullOuter(constraint)
3571 | JoinOperator::CrossJoin(constraint)
3572 | JoinOperator::Semi(constraint)
3573 | JoinOperator::LeftSemi(constraint)
3574 | JoinOperator::RightSemi(constraint)
3575 | JoinOperator::Anti(constraint)
3576 | JoinOperator::LeftAnti(constraint)
3577 | JoinOperator::RightAnti(constraint)
3578 | JoinOperator::StraightJoin(constraint) => fix_join_constraint(constraint, rule_filter),
3579 JoinOperator::AsOf {
3580 match_condition,
3581 constraint,
3582 } => {
3583 fix_expr(match_condition, rule_filter);
3584 fix_join_constraint(constraint, rule_filter);
3585 }
3586 JoinOperator::CrossApply | JoinOperator::OuterApply => {}
3587 }
3588}
3589
3590fn join_constraint_mut(join_operator: &mut JoinOperator) -> Option<&mut JoinConstraint> {
3591 match join_operator {
3592 JoinOperator::Join(constraint)
3593 | JoinOperator::Inner(constraint)
3594 | JoinOperator::Left(constraint)
3595 | JoinOperator::LeftOuter(constraint)
3596 | JoinOperator::Right(constraint)
3597 | JoinOperator::RightOuter(constraint)
3598 | JoinOperator::FullOuter(constraint)
3599 | JoinOperator::CrossJoin(constraint)
3600 | JoinOperator::Semi(constraint)
3601 | JoinOperator::LeftSemi(constraint)
3602 | JoinOperator::RightSemi(constraint)
3603 | JoinOperator::Anti(constraint)
3604 | JoinOperator::LeftAnti(constraint)
3605 | JoinOperator::RightAnti(constraint)
3606 | JoinOperator::StraightJoin(constraint) => Some(constraint),
3607 JoinOperator::AsOf { constraint, .. } => Some(constraint),
3608 JoinOperator::CrossApply | JoinOperator::OuterApply => None,
3609 }
3610}
3611
3612fn fix_join_constraint(constraint: &mut JoinConstraint, rule_filter: &RuleFilter) {
3613 if let JoinConstraint::On(expr) = constraint {
3614 fix_expr(expr, rule_filter);
3615 }
3616}
3617
3618fn fix_order_by(order_by: &mut OrderBy, rule_filter: &RuleFilter) {
3619 if let OrderByKind::Expressions(exprs) = &mut order_by.kind {
3620 for order_expr in exprs.iter_mut() {
3621 fix_expr(&mut order_expr.expr, rule_filter);
3622 }
3623 }
3624
3625 if let Some(interpolate) = order_by.interpolate.as_mut() {
3626 if let Some(exprs) = interpolate.exprs.as_mut() {
3627 for expr in exprs {
3628 if let Some(inner) = expr.expr.as_mut() {
3629 fix_expr(inner, rule_filter);
3630 }
3631 }
3632 }
3633 }
3634}
3635
3636fn fix_limit_clause(limit_clause: &mut LimitClause, rule_filter: &RuleFilter) {
3637 match limit_clause {
3638 LimitClause::LimitOffset {
3639 limit,
3640 offset,
3641 limit_by,
3642 } => {
3643 if let Some(limit) = limit {
3644 fix_expr(limit, rule_filter);
3645 }
3646 if let Some(offset) = offset {
3647 fix_expr(&mut offset.value, rule_filter);
3648 }
3649 for expr in limit_by {
3650 fix_expr(expr, rule_filter);
3651 }
3652 }
3653 LimitClause::OffsetCommaLimit { offset, limit } => {
3654 fix_expr(offset, rule_filter);
3655 fix_expr(limit, rule_filter);
3656 }
3657 }
3658}
3659
3660fn fix_expr(expr: &mut Expr, rule_filter: &RuleFilter) {
3661 match expr {
3662 Expr::BinaryOp { left, right, .. } => {
3663 fix_expr(left, rule_filter);
3664 fix_expr(right, rule_filter);
3665 }
3666 Expr::UnaryOp { expr: inner, .. }
3667 | Expr::Nested(inner)
3668 | Expr::IsNull(inner)
3669 | Expr::IsNotNull(inner)
3670 | Expr::IsTrue(inner)
3671 | Expr::IsNotTrue(inner)
3672 | Expr::IsFalse(inner)
3673 | Expr::IsNotFalse(inner)
3674 | Expr::IsUnknown(inner)
3675 | Expr::IsNotUnknown(inner) => fix_expr(inner, rule_filter),
3676 Expr::Case {
3677 operand,
3678 conditions,
3679 else_result,
3680 ..
3681 } => {
3682 if let Some(operand) = operand.as_mut() {
3683 fix_expr(operand, rule_filter);
3684 }
3685 for case_when in conditions {
3686 fix_expr(&mut case_when.condition, rule_filter);
3687 fix_expr(&mut case_when.result, rule_filter);
3688 }
3689 if let Some(else_result) = else_result.as_mut() {
3690 fix_expr(else_result, rule_filter);
3691 }
3692 }
3693 Expr::Function(func) => fix_function(func, rule_filter),
3694 Expr::Cast { expr: inner, .. } => fix_expr(inner, rule_filter),
3695 Expr::InSubquery {
3696 expr: inner,
3697 subquery,
3698 ..
3699 } => {
3700 fix_expr(inner, rule_filter);
3701 fix_query(subquery, rule_filter);
3702 }
3703 Expr::Subquery(subquery) | Expr::Exists { subquery, .. } => {
3704 fix_query(subquery, rule_filter)
3705 }
3706 Expr::Between {
3707 expr: target,
3708 low,
3709 high,
3710 ..
3711 } => {
3712 fix_expr(target, rule_filter);
3713 fix_expr(low, rule_filter);
3714 fix_expr(high, rule_filter);
3715 }
3716 Expr::InList {
3717 expr: target, list, ..
3718 } => {
3719 fix_expr(target, rule_filter);
3720 for item in list {
3721 fix_expr(item, rule_filter);
3722 }
3723 }
3724 Expr::Tuple(items) => {
3725 for item in items {
3726 fix_expr(item, rule_filter);
3727 }
3728 }
3729 _ => {}
3730 }
3731
3732 if rule_filter.allows(issue_codes::LINT_ST_004) {
3737 if let Some(rewritten) = nested_case_rewrite(expr) {
3738 *expr = rewritten;
3739 }
3740 }
3741}
3742
3743fn fix_function(func: &mut Function, rule_filter: &RuleFilter) {
3744 if let FunctionArguments::List(arg_list) = &mut func.args {
3745 for arg in &mut arg_list.args {
3746 fix_function_arg(arg, rule_filter);
3747 }
3748 for clause in &mut arg_list.clauses {
3749 match clause {
3750 FunctionArgumentClause::OrderBy(order_by_exprs) => {
3751 for order_by_expr in order_by_exprs {
3752 fix_expr(&mut order_by_expr.expr, rule_filter);
3753 }
3754 }
3755 FunctionArgumentClause::Limit(expr) => fix_expr(expr, rule_filter),
3756 _ => {}
3757 }
3758 }
3759 }
3760
3761 if let Some(filter) = func.filter.as_mut() {
3762 fix_expr(filter, rule_filter);
3763 }
3764
3765 for order_expr in &mut func.within_group {
3766 fix_expr(&mut order_expr.expr, rule_filter);
3767 }
3768}
3769
3770fn fix_function_arg(arg: &mut FunctionArg, rule_filter: &RuleFilter) {
3771 match arg {
3772 FunctionArg::Named { arg, .. }
3773 | FunctionArg::ExprNamed { arg, .. }
3774 | FunctionArg::Unnamed(arg) => {
3775 if let FunctionArgExpr::Expr(expr) = arg {
3776 fix_expr(expr, rule_filter);
3777 }
3778 }
3779 }
3780}
3781
3782fn nested_case_rewrite(expr: &Expr) -> Option<Expr> {
3783 let Expr::Case {
3784 case_token,
3785 operand: outer_operand,
3786 conditions: outer_conditions,
3787 else_result: Some(outer_else),
3788 end_token,
3789 } = expr
3790 else {
3791 return None;
3792 };
3793
3794 if outer_conditions.is_empty() {
3795 return None;
3796 }
3797
3798 let Expr::Case {
3799 operand: inner_operand,
3800 conditions: inner_conditions,
3801 else_result: inner_else,
3802 ..
3803 } = nested_case_expr(outer_else.as_ref())?
3804 else {
3805 return None;
3806 };
3807
3808 if inner_conditions.is_empty() {
3809 return None;
3810 }
3811
3812 if !case_operands_match(outer_operand.as_deref(), inner_operand.as_deref()) {
3813 return None;
3814 }
3815
3816 let mut merged_conditions = outer_conditions.clone();
3817 merged_conditions.extend(inner_conditions.iter().cloned());
3818
3819 Some(Expr::Case {
3820 case_token: case_token.clone(),
3821 operand: outer_operand.clone(),
3822 conditions: merged_conditions,
3823 else_result: inner_else.clone(),
3824 end_token: end_token.clone(),
3825 })
3826}
3827
3828fn nested_case_expr(expr: &Expr) -> Option<&Expr> {
3829 match expr {
3830 Expr::Case { .. } => Some(expr),
3831 Expr::Nested(inner) => nested_case_expr(inner),
3832 _ => None,
3833 }
3834}
3835
3836fn case_operands_match(outer: Option<&Expr>, inner: Option<&Expr>) -> bool {
3837 match (outer, inner) {
3838 (None, None) => true,
3839 (Some(left), Some(right)) => format!("{left}") == format!("{right}"),
3840 _ => false,
3841 }
3842}
3843
3844#[cfg(test)]
3845mod tests {
3846 use super::*;
3847 use flowscope_core::{
3848 analyze, issue_codes, AnalysisOptions, AnalyzeRequest, Dialect, LintConfig,
3849 };
3850
3851 fn default_lint_config() -> LintConfig {
3852 LintConfig {
3853 enabled: true,
3854 disabled_rules: vec![],
3855 rule_configs: std::collections::BTreeMap::new(),
3856 }
3857 }
3858
3859 fn lint_config_keep_only_rule(rule_code: &str, mut config: LintConfig) -> LintConfig {
3860 let disabled_rules = flowscope_core::linter::rules::all_rules(&default_lint_config())
3861 .into_iter()
3862 .map(|rule| rule.code().to_string())
3863 .filter(|code| !code.eq_ignore_ascii_case(rule_code))
3864 .collect();
3865 config.disabled_rules = disabled_rules;
3866 config
3867 }
3868
3869 fn lint_rule_count_with_config(sql: &str, code: &str, lint_config: &LintConfig) -> usize {
3870 let request = AnalyzeRequest {
3871 sql: sql.to_string(),
3872 files: None,
3873 dialect: Dialect::Generic,
3874 source_name: None,
3875 options: Some(AnalysisOptions {
3876 lint: Some(lint_config.clone()),
3877 ..Default::default()
3878 }),
3879 schema: None,
3880 #[cfg(feature = "templating")]
3881 template_config: None,
3882 };
3883
3884 analyze(&request)
3885 .issues
3886 .iter()
3887 .filter(|issue| issue.code == code)
3888 .count()
3889 }
3890
3891 fn lint_rule_count_with_config_in_dialect(
3892 sql: &str,
3893 code: &str,
3894 dialect: Dialect,
3895 lint_config: &LintConfig,
3896 ) -> usize {
3897 let request = AnalyzeRequest {
3898 sql: sql.to_string(),
3899 files: None,
3900 dialect,
3901 source_name: None,
3902 options: Some(AnalysisOptions {
3903 lint: Some(lint_config.clone()),
3904 ..Default::default()
3905 }),
3906 schema: None,
3907 #[cfg(feature = "templating")]
3908 template_config: None,
3909 };
3910
3911 analyze(&request)
3912 .issues
3913 .iter()
3914 .filter(|issue| issue.code == code)
3915 .count()
3916 }
3917
3918 fn lint_rule_count(sql: &str, code: &str) -> usize {
3919 lint_rule_count_with_config(sql, code, &default_lint_config())
3920 }
3921
3922 fn apply_fix_with_config(sql: &str, lint_config: &LintConfig) -> FixOutcome {
3923 apply_lint_fixes_with_lint_config(sql, Dialect::Generic, lint_config).expect("fix result")
3924 }
3925
3926 fn apply_core_only_fixes(sql: &str) -> FixOutcome {
3927 apply_lint_fixes_with_options(
3928 sql,
3929 Dialect::Generic,
3930 &default_lint_config(),
3931 FixOptions {
3932 include_unsafe_fixes: true,
3933 include_rewrite_candidates: false,
3934 },
3935 )
3936 .expect("fix result")
3937 }
3938
3939 fn sample_outcome(skipped_counts: FixSkippedCounts) -> FixOutcome {
3940 FixOutcome {
3941 sql: String::new(),
3942 counts: FixCounts::default(),
3943 changed: false,
3944 skipped_due_to_comments: false,
3945 skipped_due_to_regression: false,
3946 skipped_counts,
3947 }
3948 }
3949
3950 #[test]
3951 fn collect_fix_candidate_stats_always_counts_display_only_as_blocked() {
3952 let outcome = sample_outcome(FixSkippedCounts {
3953 unsafe_skipped: 1,
3954 protected_range_blocked: 2,
3955 overlap_conflict_blocked: 3,
3956 display_only: 4,
3957 });
3958
3959 let stats = collect_fix_candidate_stats(
3960 &outcome,
3961 LintFixRuntimeOptions {
3962 include_unsafe_fixes: false,
3963 legacy_ast_fixes: false,
3964 },
3965 );
3966
3967 assert_eq!(stats.skipped, 0);
3968 assert_eq!(stats.blocked, 10);
3969 assert_eq!(stats.blocked_unsafe, 1);
3970 assert_eq!(stats.blocked_display_only, 4);
3971 assert_eq!(stats.blocked_protected_range, 2);
3972 assert_eq!(stats.blocked_overlap_conflict, 3);
3973 }
3974
3975 #[test]
3976 fn collect_fix_candidate_stats_excludes_unsafe_when_unsafe_fixes_enabled() {
3977 let outcome = sample_outcome(FixSkippedCounts {
3978 unsafe_skipped: 2,
3979 protected_range_blocked: 1,
3980 overlap_conflict_blocked: 1,
3981 display_only: 3,
3982 });
3983
3984 let stats = collect_fix_candidate_stats(
3985 &outcome,
3986 LintFixRuntimeOptions {
3987 include_unsafe_fixes: true,
3988 legacy_ast_fixes: false,
3989 },
3990 );
3991
3992 assert_eq!(stats.blocked, 5);
3993 assert_eq!(stats.blocked_unsafe, 0);
3994 assert_eq!(stats.blocked_display_only, 3);
3995 }
3996
3997 #[test]
3998 fn mostly_unfixable_residual_detects_dominated_known_residuals() {
3999 let counts = std::collections::BTreeMap::from([
4000 (issue_codes::LINT_LT_005.to_string(), 140usize),
4001 (issue_codes::LINT_RF_002.to_string(), 116usize),
4002 (issue_codes::LINT_AL_003.to_string(), 43usize),
4003 (issue_codes::LINT_RF_004.to_string(), 2usize),
4004 (issue_codes::LINT_ST_009.to_string(), 1usize),
4005 ]);
4006 assert!(is_mostly_unfixable_residual(&counts));
4007 }
4008
4009 #[test]
4010 fn mostly_unfixable_residual_rejects_when_fixable_tail_is_material() {
4011 let counts = std::collections::BTreeMap::from([
4012 (issue_codes::LINT_LT_005.to_string(), 20usize),
4013 (issue_codes::LINT_RF_002.to_string(), 10usize),
4014 (issue_codes::LINT_ST_009.to_string(), 8usize),
4015 (issue_codes::LINT_LT_003.to_string(), 3usize),
4016 ]);
4017 assert!(!is_mostly_unfixable_residual(&counts));
4018 }
4019
4020 #[test]
4021 fn am005_outer_mode_full_join_fix_output() {
4022 let lint_config = LintConfig {
4023 enabled: true,
4024 disabled_rules: vec![issue_codes::LINT_CV_008.to_string()],
4025 rule_configs: std::collections::BTreeMap::from([(
4026 "ambiguous.join".to_string(),
4027 serde_json::json!({"fully_qualify_join_types": "outer"}),
4028 )]),
4029 };
4030 let sql = "SELECT a FROM t FULL JOIN u ON t.id = u.id";
4031 assert_eq!(
4032 lint_rule_count_with_config(
4033 "SELECT a FROM t FULL OUTER JOIN u ON t.id = u.id",
4034 issue_codes::LINT_AM_005,
4035 &lint_config,
4036 ),
4037 0
4038 );
4039 let out = apply_fix_with_config(sql, &lint_config);
4040 assert!(
4041 out.sql.to_ascii_uppercase().contains("FULL OUTER JOIN"),
4042 "expected FULL OUTER JOIN in fixed SQL, got: {}",
4043 out.sql
4044 );
4045 assert_eq!(fix_count_for_code(&out.counts, issue_codes::LINT_AM_005), 1);
4046 }
4047
4048 fn fix_count_for_code(counts: &FixCounts, code: &str) -> usize {
4049 counts.get(code)
4050 }
4051
4052 #[test]
4053 fn lint_rule_counts_includes_parse_errors() {
4054 let counts = lint_rule_counts("SELECT (", Dialect::Generic, &default_lint_config());
4055 assert!(
4056 counts.get(issue_codes::PARSE_ERROR).copied().unwrap_or(0) > 0,
4057 "invalid SQL should contribute PARSE_ERROR to regression counts"
4058 );
4059 }
4060
4061 #[test]
4062 fn parse_error_regression_is_detected_even_with_lint_improvements() {
4063 let before = std::collections::BTreeMap::from([(issue_codes::LINT_ST_005.to_string(), 1)]);
4064 let after = std::collections::BTreeMap::from([(issue_codes::PARSE_ERROR.to_string(), 1)]);
4065 let removed = FixCounts::from_removed(&before, &after);
4066
4067 assert_eq!(
4068 removed.total(),
4069 1,
4070 "lint-only comparison can still look improved"
4071 );
4072 assert!(
4073 parse_errors_increased(&before, &after),
4074 "introduced parse errors must force regression"
4075 );
4076 }
4077
4078 #[test]
4079 fn lint_improvements_can_mask_total_violation_regressions() {
4080 let before = std::collections::BTreeMap::from([
4081 (issue_codes::LINT_LT_002.to_string(), 2usize),
4082 (issue_codes::LINT_LT_001.to_string(), 0usize),
4083 ]);
4084 let after = std::collections::BTreeMap::from([
4085 (issue_codes::LINT_LT_002.to_string(), 1usize),
4086 (issue_codes::LINT_LT_001.to_string(), 2usize),
4087 ]);
4088 let removed = FixCounts::from_removed(&before, &after);
4089 let before_total: usize = before.values().sum();
4090 let after_total: usize = after.values().sum();
4091
4092 assert_eq!(
4093 removed.total(),
4094 1,
4095 "a rule-level improvement can still be observed"
4096 );
4097 assert!(
4098 after_total > before_total,
4099 "strict regression guard must reject net-violation increases"
4100 );
4101 }
4102
4103 #[test]
4104 fn lt03_improvement_allows_lt05_tradeoff_at_equal_totals() {
4105 let before = std::collections::BTreeMap::from([
4106 (issue_codes::LINT_LT_003.to_string(), 1usize),
4107 (issue_codes::LINT_LT_005.to_string(), 5usize),
4108 ]);
4109 let after = std::collections::BTreeMap::from([
4110 (issue_codes::LINT_LT_003.to_string(), 0usize),
4111 (issue_codes::LINT_LT_005.to_string(), 6usize),
4112 ]);
4113 let core_rules = std::collections::HashSet::from([
4114 issue_codes::LINT_LT_003.to_string(),
4115 issue_codes::LINT_LT_005.to_string(),
4116 ]);
4117
4118 assert!(
4119 !core_autofix_rules_not_improved(&before, &after, &core_rules),
4120 "LT03 improvements should be allowed to trade against LT05 at equal totals"
4121 );
4122 }
4123
4124 #[test]
4125 fn lt05_tradeoff_is_not_allowed_without_lt03_improvement() {
4126 let before = std::collections::BTreeMap::from([
4127 (issue_codes::LINT_LT_003.to_string(), 1usize),
4128 (issue_codes::LINT_LT_005.to_string(), 5usize),
4129 ]);
4130 let after = std::collections::BTreeMap::from([
4131 (issue_codes::LINT_LT_003.to_string(), 1usize),
4132 (issue_codes::LINT_LT_005.to_string(), 6usize),
4133 ]);
4134 let core_rules = std::collections::HashSet::from([
4135 issue_codes::LINT_LT_003.to_string(),
4136 issue_codes::LINT_LT_005.to_string(),
4137 ]);
4138
4139 assert!(
4140 core_autofix_rules_not_improved(&before, &after, &core_rules),
4141 "without LT03 improvement, LT05 worsening remains blocked"
4142 );
4143 }
4144
4145 fn assert_rule_case(
4146 sql: &str,
4147 code: &str,
4148 expected_before: usize,
4149 expected_after: usize,
4150 expected_fix_count: usize,
4151 ) {
4152 let before = lint_rule_count(sql, code);
4153 assert_eq!(
4154 before, expected_before,
4155 "unexpected initial lint count for {code} in SQL: {sql}"
4156 );
4157
4158 let out = apply_core_only_fixes(sql);
4159 assert!(
4160 !out.skipped_due_to_comments,
4161 "test SQL should not be skipped"
4162 );
4163 assert_eq!(
4164 fix_count_for_code(&out.counts, code),
4165 expected_fix_count,
4166 "unexpected fix count for {code} in SQL: {sql}"
4167 );
4168
4169 if expected_fix_count > 0 {
4170 assert!(out.changed, "expected SQL to change for {code}: {sql}");
4171 }
4172
4173 let after = lint_rule_count(&out.sql, code);
4174 assert_eq!(
4175 after, expected_after,
4176 "unexpected lint count after fix for {code}. SQL: {}",
4177 out.sql
4178 );
4179
4180 let second_pass = apply_core_only_fixes(&out.sql);
4181 assert_eq!(
4182 fix_count_for_code(&second_pass.counts, code),
4183 0,
4184 "expected idempotent second pass for {code}"
4185 );
4186 }
4187
4188 fn assert_rule_case_with_config(
4189 sql: &str,
4190 code: &str,
4191 expected_before: usize,
4192 expected_after: usize,
4193 expected_fix_count: usize,
4194 lint_config: &LintConfig,
4195 ) {
4196 let before = lint_rule_count_with_config(sql, code, lint_config);
4197 assert_eq!(
4198 before, expected_before,
4199 "unexpected initial lint count for {code} in SQL: {sql}"
4200 );
4201
4202 let out = apply_fix_with_config(sql, lint_config);
4203 assert!(
4204 !out.skipped_due_to_comments,
4205 "test SQL should not be skipped"
4206 );
4207 assert_eq!(
4208 fix_count_for_code(&out.counts, code),
4209 expected_fix_count,
4210 "unexpected fix count for {code} in SQL: {sql}"
4211 );
4212
4213 if expected_fix_count > 0 {
4214 assert!(out.changed, "expected SQL to change for {code}: {sql}");
4215 }
4216
4217 let after = lint_rule_count_with_config(&out.sql, code, lint_config);
4218 assert_eq!(
4219 after, expected_after,
4220 "unexpected lint count after fix for {code}. SQL: {}",
4221 out.sql
4222 );
4223
4224 let second_pass = apply_fix_with_config(&out.sql, lint_config);
4225 assert_eq!(
4226 fix_count_for_code(&second_pass.counts, code),
4227 0,
4228 "expected idempotent second pass for {code}"
4229 );
4230 }
4231
4232 #[test]
4233 fn sqlfluff_am003_cases_are_fixed() {
4234 let cases = [
4235 ("SELECT DISTINCT col FROM t GROUP BY col", 1, 0, 1),
4236 (
4237 "SELECT * FROM (SELECT DISTINCT a FROM t GROUP BY a) AS sub",
4238 1,
4239 0,
4240 1,
4241 ),
4242 (
4243 "WITH cte AS (SELECT DISTINCT a FROM t GROUP BY a) SELECT * FROM cte",
4244 1,
4245 0,
4246 1,
4247 ),
4248 (
4249 "CREATE VIEW v AS SELECT DISTINCT a FROM t GROUP BY a",
4250 1,
4251 0,
4252 1,
4253 ),
4254 (
4255 "INSERT INTO target SELECT DISTINCT a FROM t GROUP BY a",
4256 1,
4257 0,
4258 1,
4259 ),
4260 (
4261 "SELECT a FROM t UNION ALL SELECT DISTINCT b FROM t2 GROUP BY b",
4262 1,
4263 0,
4264 1,
4265 ),
4266 ("SELECT a, b FROM t", 0, 0, 0),
4267 ];
4268
4269 for (sql, before, after, fix_count) in cases {
4270 assert_rule_case(sql, issue_codes::LINT_AM_001, before, after, fix_count);
4271 }
4272 }
4273
4274 #[test]
4275 fn sqlfluff_am001_cases_are_fixed_or_unchanged() {
4276 let lint_config = LintConfig {
4277 enabled: true,
4278 disabled_rules: vec![issue_codes::LINT_LT_011.to_string()],
4279 rule_configs: std::collections::BTreeMap::new(),
4280 };
4281 let cases = [
4282 (
4283 "SELECT a, b FROM tbl UNION SELECT c, d FROM tbl1",
4284 1,
4285 0,
4286 1,
4287 Some("DISTINCT SELECT"),
4288 ),
4289 (
4290 "SELECT a, b FROM tbl UNION ALL SELECT c, d FROM tbl1",
4291 0,
4292 0,
4293 0,
4294 None,
4295 ),
4296 (
4297 "SELECT a, b FROM tbl UNION DISTINCT SELECT c, d FROM tbl1",
4298 0,
4299 0,
4300 0,
4301 None,
4302 ),
4303 (
4304 "select a, b from tbl union select c, d from tbl1",
4305 1,
4306 0,
4307 1,
4308 Some("DISTINCT SELECT"),
4309 ),
4310 ];
4311
4312 for (sql, before, after, fix_count, expected_text) in cases {
4313 assert_rule_case_with_config(
4314 sql,
4315 issue_codes::LINT_AM_002,
4316 before,
4317 after,
4318 fix_count,
4319 &lint_config,
4320 );
4321
4322 if let Some(expected) = expected_text {
4323 let out = apply_fix_with_config(sql, &lint_config);
4324 assert!(
4325 out.sql.to_ascii_uppercase().contains(expected),
4326 "expected {expected:?} in fixed SQL, got: {}",
4327 out.sql
4328 );
4329 }
4330 }
4331 }
4332
4333 #[test]
4334 fn sqlfluff_am005_cases_are_fixed_or_unchanged() {
4335 let cases = [
4336 (
4337 "SELECT * FROM t ORDER BY a, b DESC",
4338 1,
4339 0,
4340 1,
4341 Some("ORDER BY A ASC, B DESC"),
4342 ),
4343 (
4344 "SELECT * FROM t ORDER BY a DESC, b",
4345 1,
4346 0,
4347 1,
4348 Some("ORDER BY A DESC, B ASC"),
4349 ),
4350 (
4351 "SELECT * FROM t ORDER BY a DESC, b NULLS LAST",
4352 1,
4353 0,
4354 1,
4355 Some("ORDER BY A DESC, B ASC NULLS LAST"),
4356 ),
4357 ("SELECT * FROM t ORDER BY a, b", 0, 0, 0, None),
4358 ("SELECT * FROM t ORDER BY a ASC, b DESC", 0, 0, 0, None),
4359 ];
4360
4361 for (sql, before, after, fix_count, expected_text) in cases {
4362 assert_rule_case(sql, issue_codes::LINT_AM_003, before, after, fix_count);
4363
4364 if let Some(expected) = expected_text {
4365 let out = apply_lint_fixes(sql, Dialect::Generic, &[]).expect("fix result");
4366 assert!(
4367 out.sql.to_ascii_uppercase().contains(expected),
4368 "expected {expected:?} in fixed SQL, got: {}",
4369 out.sql
4370 );
4371 }
4372 }
4373 }
4374
4375 #[test]
4376 fn sqlfluff_am006_cases_are_fixed_or_unchanged() {
4377 let cases = [
4378 (
4379 "SELECT a FROM t JOIN u ON t.id = u.id",
4380 1,
4381 0,
4382 1,
4383 Some("INNER JOIN"),
4384 ),
4385 (
4386 "SELECT a FROM t JOIN u ON t.id = u.id JOIN v ON u.id = v.id",
4387 2,
4388 0,
4389 2,
4390 Some("INNER JOIN U"),
4391 ),
4392 ("SELECT a FROM t INNER JOIN u ON t.id = u.id", 0, 0, 0, None),
4393 ("SELECT a FROM t LEFT JOIN u ON t.id = u.id", 0, 0, 0, None),
4394 ];
4395
4396 for (sql, before, after, fix_count, expected_text) in cases {
4397 assert_rule_case(sql, issue_codes::LINT_AM_005, before, after, fix_count);
4398
4399 if let Some(expected) = expected_text {
4400 let out = apply_lint_fixes(sql, Dialect::Generic, &[]).expect("fix result");
4401 assert!(
4402 out.sql.to_ascii_uppercase().contains(expected),
4403 "expected {expected:?} in fixed SQL, got: {}",
4404 out.sql
4405 );
4406 }
4407 }
4408 }
4409
4410 #[test]
4411 fn sqlfluff_am005_outer_and_both_configs_are_fixed() {
4412 let outer_config = LintConfig {
4413 enabled: true,
4414 disabled_rules: vec![issue_codes::LINT_CV_008.to_string()],
4415 rule_configs: std::collections::BTreeMap::from([(
4416 "ambiguous.join".to_string(),
4417 serde_json::json!({"fully_qualify_join_types": "outer"}),
4418 )]),
4419 };
4420 let both_config = LintConfig {
4421 enabled: true,
4422 disabled_rules: vec![issue_codes::LINT_CV_008.to_string()],
4423 rule_configs: std::collections::BTreeMap::from([(
4424 "ambiguous.join".to_string(),
4425 serde_json::json!({"fully_qualify_join_types": "both"}),
4426 )]),
4427 };
4428
4429 let outer_cases = [
4430 (
4431 "SELECT a FROM t LEFT JOIN u ON t.id = u.id",
4432 1,
4433 0,
4434 1,
4435 Some("LEFT OUTER JOIN"),
4436 ),
4437 (
4438 "SELECT a FROM t RIGHT JOIN u ON t.id = u.id",
4439 1,
4440 0,
4441 1,
4442 Some("RIGHT OUTER JOIN"),
4443 ),
4444 (
4445 "SELECT a FROM t FULL JOIN u ON t.id = u.id",
4446 1,
4447 0,
4448 1,
4449 Some("FULL OUTER JOIN"),
4450 ),
4451 (
4452 "SELECT a FROM t full join u ON t.id = u.id",
4453 1,
4454 0,
4455 1,
4456 Some("FULL OUTER JOIN"),
4457 ),
4458 ("SELECT a FROM t JOIN u ON t.id = u.id", 0, 0, 0, None),
4459 ];
4460 for (sql, before, after, fix_count, expected_text) in outer_cases {
4461 assert_rule_case_with_config(
4462 sql,
4463 issue_codes::LINT_AM_005,
4464 before,
4465 after,
4466 fix_count,
4467 &outer_config,
4468 );
4469 if let Some(expected) = expected_text {
4470 let out = apply_fix_with_config(sql, &outer_config);
4471 assert!(
4472 out.sql.to_ascii_uppercase().contains(expected),
4473 "expected {expected:?} in fixed SQL, got: {}",
4474 out.sql
4475 );
4476 }
4477 }
4478
4479 let both_cases = [
4480 (
4481 "SELECT a FROM t JOIN u ON t.id = u.id",
4482 1,
4483 0,
4484 1,
4485 Some("INNER JOIN"),
4486 ),
4487 (
4488 "SELECT a FROM t LEFT JOIN u ON t.id = u.id",
4489 1,
4490 0,
4491 1,
4492 Some("LEFT OUTER JOIN"),
4493 ),
4494 (
4495 "SELECT a FROM t FULL JOIN u ON t.id = u.id",
4496 1,
4497 0,
4498 1,
4499 Some("FULL OUTER JOIN"),
4500 ),
4501 ];
4502 for (sql, before, after, fix_count, expected_text) in both_cases {
4503 assert_rule_case_with_config(
4504 sql,
4505 issue_codes::LINT_AM_005,
4506 before,
4507 after,
4508 fix_count,
4509 &both_config,
4510 );
4511 if let Some(expected) = expected_text {
4512 let out = apply_fix_with_config(sql, &both_config);
4513 assert!(
4514 out.sql.to_ascii_uppercase().contains(expected),
4515 "expected {expected:?} in fixed SQL, got: {}",
4516 out.sql
4517 );
4518 }
4519 }
4520 }
4521
4522 #[test]
4523 fn sqlfluff_am009_cases_are_fixed_or_unchanged() {
4524 let cases = [
4525 (
4526 "SELECT foo.a, bar.b FROM foo INNER JOIN bar",
4527 1,
4528 0,
4529 1,
4530 Some("CROSS JOIN BAR"),
4531 ),
4532 (
4533 "SELECT foo.a, bar.b FROM foo LEFT JOIN bar",
4534 1,
4535 0,
4536 1,
4537 Some("CROSS JOIN BAR"),
4538 ),
4539 (
4540 "SELECT foo.a, bar.b FROM foo JOIN bar WHERE foo.a = bar.a OR foo.x = 3",
4541 0,
4542 0,
4543 0,
4544 None,
4545 ),
4546 ("SELECT foo.a, bar.b FROM foo CROSS JOIN bar", 0, 0, 0, None),
4547 (
4548 "SELECT foo.id, bar.id FROM foo LEFT JOIN bar USING (id)",
4549 0,
4550 0,
4551 0,
4552 None,
4553 ),
4554 ];
4555
4556 for (sql, before, after, fix_count, expected_text) in cases {
4557 assert_rule_case(sql, issue_codes::LINT_AM_008, before, after, fix_count);
4558
4559 if let Some(expected) = expected_text {
4560 let out = apply_lint_fixes(sql, Dialect::Generic, &[]).expect("fix result");
4561 assert!(
4562 out.sql.to_ascii_uppercase().contains(expected),
4563 "expected {expected:?} in fixed SQL, got: {}",
4564 out.sql
4565 );
4566 }
4567 }
4568 }
4569
4570 #[test]
4571 fn sqlfluff_al007_force_enabled_single_table_alias_is_fixed() {
4572 let lint_config = LintConfig {
4573 enabled: true,
4574 disabled_rules: vec![],
4575 rule_configs: std::collections::BTreeMap::from([(
4576 "aliasing.forbid".to_string(),
4577 serde_json::json!({"force_enable": true}),
4578 )]),
4579 };
4580 let sql = "SELECT u.id FROM users u";
4581 assert_rule_case_with_config(sql, issue_codes::LINT_AL_007, 1, 0, 1, &lint_config);
4582
4583 let out = apply_fix_with_config(sql, &lint_config);
4584 let fixed_upper = out.sql.to_ascii_uppercase();
4585 assert!(
4586 fixed_upper.contains("FROM USERS"),
4587 "expected table alias to be removed: {}",
4588 out.sql
4589 );
4590 assert!(
4591 !fixed_upper.contains("FROM USERS U"),
4592 "expected unnecessary table alias to be removed: {}",
4593 out.sql
4594 );
4595 assert!(
4596 fixed_upper.contains("USERS.ID"),
4597 "expected references to use table name after alias removal: {}",
4598 out.sql
4599 );
4600 }
4601
4602 #[test]
4603 fn sqlfluff_al009_fix_respects_case_sensitive_mode() {
4604 let lint_config = LintConfig {
4605 enabled: true,
4606 disabled_rules: vec![issue_codes::LINT_CP_002.to_string()],
4609 rule_configs: std::collections::BTreeMap::from([(
4610 "aliasing.self_alias.column".to_string(),
4611 serde_json::json!({"alias_case_check": "case_sensitive"}),
4612 )]),
4613 };
4614 let sql = "SELECT a AS A FROM t";
4615 assert_rule_case_with_config(sql, issue_codes::LINT_AL_009, 0, 0, 0, &lint_config);
4616
4617 let out = apply_fix_with_config(sql, &lint_config);
4618 assert!(
4619 out.sql.contains("AS A"),
4620 "case-sensitive mode should keep case-mismatched alias: {}",
4621 out.sql
4622 );
4623 }
4624
4625 #[test]
4626 fn sqlfluff_al009_ast_fix_keeps_table_aliases() {
4627 let lint_config = LintConfig {
4628 enabled: true,
4629 disabled_rules: vec![issue_codes::LINT_AL_007.to_string()],
4630 rule_configs: std::collections::BTreeMap::new(),
4631 };
4632 let sql = "SELECT t.a AS a FROM t AS t";
4633 assert_rule_case_with_config(sql, issue_codes::LINT_AL_009, 1, 0, 1, &lint_config);
4634
4635 let out = apply_fix_with_config(sql, &lint_config);
4636 let fixed_upper = out.sql.to_ascii_uppercase();
4637 assert!(
4638 fixed_upper.contains("FROM T AS T"),
4639 "AL09 fix should not remove table alias declarations: {}",
4640 out.sql
4641 );
4642 assert!(
4643 !fixed_upper.contains("T.A AS A"),
4644 "expected only column self-alias to be removed: {}",
4645 out.sql
4646 );
4647 }
4648
4649 #[test]
4650 fn sqlfluff_st002_unnecessary_case_fix_cases() {
4651 let cases = [
4652 (
4654 "SELECT CASE WHEN x > 0 THEN true ELSE false END FROM t",
4655 1,
4656 0,
4657 1,
4658 Some("COALESCE(X > 0, FALSE)"),
4659 ),
4660 (
4662 "SELECT CASE WHEN x > 0 THEN false ELSE true END FROM t",
4663 1,
4664 0,
4665 1,
4666 Some("NOT COALESCE(X > 0, FALSE)"),
4667 ),
4668 (
4670 "SELECT CASE WHEN x IS NULL THEN 0 ELSE x END FROM t",
4671 1,
4672 0,
4673 1,
4674 Some("COALESCE(X, 0)"),
4675 ),
4676 (
4678 "SELECT CASE WHEN x = 1 THEN 'a' WHEN x = 2 THEN 'b' END FROM t",
4679 0,
4680 0,
4681 0,
4682 None,
4683 ),
4684 ];
4685
4686 for (sql, before, after, fix_count, expected_text) in cases {
4687 assert_rule_case(sql, issue_codes::LINT_ST_002, before, after, fix_count);
4688
4689 if let Some(expected) = expected_text {
4690 let out = apply_lint_fixes(sql, Dialect::Generic, &[]).expect("fix result");
4691 assert!(
4692 out.sql.to_ascii_uppercase().contains(expected),
4693 "expected {expected:?} in fixed SQL, got: {}",
4694 out.sql
4695 );
4696 }
4697 }
4698 }
4699
4700 #[test]
4701 fn sqlfluff_st006_cases_are_fixed_or_unchanged() {
4702 let cases = [
4703 ("SELECT a + 1, a FROM t", 1, 0, 1, Some("A,\n A + 1")),
4704 (
4705 "SELECT a + 1, b + 2, a FROM t",
4706 1,
4707 0,
4708 1,
4709 Some("A,\n A + 1,\n B + 2"),
4710 ),
4711 (
4712 "SELECT a + 1, b AS b_alias FROM t",
4713 1,
4714 0,
4715 1,
4716 Some("B AS B_ALIAS,\n A + 1"),
4717 ),
4718 ("SELECT a, b + 1 FROM t", 0, 0, 0, None),
4719 ("SELECT a + 1, b + 2 FROM t", 0, 0, 0, None),
4720 ];
4721
4722 for (sql, before, after, fix_count, expected_text) in cases {
4723 assert_rule_case(sql, issue_codes::LINT_ST_006, before, after, fix_count);
4724
4725 if let Some(expected) = expected_text {
4726 let out = apply_core_only_fixes(sql);
4727 assert!(
4728 out.sql.to_ascii_uppercase().contains(expected),
4729 "expected {expected:?} in fixed SQL, got: {}",
4730 out.sql
4731 );
4732 }
4733 }
4734 }
4735
4736 #[test]
4737 fn sqlfluff_st008_cases_are_fixed_or_unchanged() {
4738 let cases = [
4739 (
4740 "SELECT DISTINCT(a) FROM t",
4741 1,
4742 0,
4743 1,
4744 Some("SELECT DISTINCT A"),
4745 ),
4746 ("SELECT DISTINCT a FROM t", 0, 0, 0, None),
4747 ("SELECT a FROM t", 0, 0, 0, None),
4748 ];
4749
4750 for (sql, before, after, fix_count, expected_text) in cases {
4751 assert_rule_case(sql, issue_codes::LINT_ST_008, before, after, fix_count);
4752
4753 if let Some(expected) = expected_text {
4754 let out = apply_lint_fixes(sql, Dialect::Generic, &[]).expect("fix result");
4755 assert!(
4756 out.sql.to_ascii_uppercase().contains(expected),
4757 "expected {expected:?} in fixed SQL, got: {}",
4758 out.sql
4759 );
4760 }
4761 }
4762 }
4763
4764 #[test]
4765 fn sqlfluff_st009_cases_are_fixed_or_unchanged() {
4766 let cases = [
4767 (
4768 "SELECT foo.a, bar.b FROM foo LEFT JOIN bar ON bar.a = foo.a",
4769 1,
4770 0,
4771 1,
4772 Some("ON FOO.A = BAR.A"),
4773 ),
4774 (
4775 "SELECT foo.a, foo.b, bar.c FROM foo LEFT JOIN bar ON bar.a = foo.a AND bar.b = foo.b",
4776 1,
4777 1,
4778 0,
4779 None,
4780 ),
4781 (
4782 "SELECT foo.a, bar.b FROM foo LEFT JOIN bar ON foo.a = bar.a",
4783 0,
4784 0,
4785 0,
4786 None,
4787 ),
4788 (
4789 "SELECT foo.a, bar.b FROM foo LEFT JOIN bar ON bar.b = a",
4790 0,
4791 0,
4792 0,
4793 None,
4794 ),
4795 (
4796 "SELECT foo.a, bar.b FROM foo AS x LEFT JOIN bar AS y ON y.a = x.a",
4797 1,
4798 0,
4799 1,
4800 Some("ON X.A = Y.A"),
4801 ),
4802 ];
4803
4804 for (sql, before, after, fix_count, expected_text) in cases {
4805 if before == after && fix_count == 0 {
4806 let initial = lint_rule_count(sql, issue_codes::LINT_ST_009);
4807 assert_eq!(
4808 initial,
4809 before,
4810 "unexpected initial lint count for {} in SQL: {}",
4811 issue_codes::LINT_ST_009,
4812 sql
4813 );
4814
4815 let out = apply_lint_fixes(sql, Dialect::Generic, &[]).expect("fix result");
4816 assert_eq!(
4817 fix_count_for_code(&out.counts, issue_codes::LINT_ST_009),
4818 0,
4819 "unexpected fix count for {} in SQL: {}",
4820 issue_codes::LINT_ST_009,
4821 sql
4822 );
4823 let after_count = lint_rule_count(&out.sql, issue_codes::LINT_ST_009);
4824 assert_eq!(
4825 after_count,
4826 after,
4827 "unexpected lint count after fix for {}. SQL: {}",
4828 issue_codes::LINT_ST_009,
4829 out.sql
4830 );
4831 } else {
4832 assert_rule_case(sql, issue_codes::LINT_ST_009, before, after, fix_count);
4833 }
4834
4835 if let Some(expected) = expected_text {
4836 let out = apply_lint_fixes(sql, Dialect::Generic, &[]).expect("fix result");
4837 assert!(
4838 out.sql.to_ascii_uppercase().contains(expected),
4839 "expected {expected:?} in fixed SQL, got: {}",
4840 out.sql
4841 );
4842 }
4843 }
4844 }
4845
4846 #[test]
4847 fn sqlfluff_st007_cases_are_fixed_or_unchanged() {
4848 let cases = [
4849 (
4850 "SELECT * FROM a JOIN b USING (id)",
4851 1,
4852 0,
4853 1,
4854 Some("ON A.ID = B.ID"),
4855 ),
4856 (
4857 "SELECT * FROM a AS x JOIN b AS y USING (id)",
4858 1,
4859 0,
4860 1,
4861 Some("ON X.ID = Y.ID"),
4862 ),
4863 (
4864 "SELECT * FROM a JOIN b USING (id, tenant_id)",
4865 1,
4866 0,
4867 1,
4868 Some("ON A.ID = B.ID AND A.TENANT_ID = B.TENANT_ID"),
4869 ),
4870 ("SELECT * FROM a JOIN b ON a.id = b.id", 0, 0, 0, None),
4871 ];
4872
4873 for (sql, before, after, fix_count, expected_text) in cases {
4874 assert_rule_case(sql, issue_codes::LINT_ST_007, before, after, fix_count);
4875
4876 if let Some(expected) = expected_text {
4877 let out = apply_lint_fixes(sql, Dialect::Generic, &[]).expect("fix result");
4878 assert!(
4879 out.sql.to_ascii_uppercase().contains(expected),
4880 "expected {expected:?} in fixed SQL, got: {}",
4881 out.sql
4882 );
4883 }
4884 }
4885 }
4886
4887 #[test]
4888 fn sqlfluff_st004_cases_are_fixed_or_unchanged() {
4889 let cases = [
4890 (
4891 "SELECT CASE WHEN species = 'Rat' THEN 'Squeak' ELSE CASE WHEN species = 'Dog' THEN 'Woof' END END AS sound FROM mytable",
4892 1,
4893 1,
4894 0,
4895 ),
4896 (
4897 "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",
4898 1,
4899 1,
4900 0,
4901 ),
4902 (
4903 "SELECT CASE WHEN species = 'Rat' THEN CASE WHEN colour = 'Black' THEN 'Growl' WHEN colour = 'Grey' THEN 'Squeak' END END AS sound FROM mytable",
4904 0,
4905 0,
4906 0,
4907 ),
4908 (
4909 "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",
4910 0,
4911 0,
4912 0,
4913 ),
4914 (
4915 "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",
4916 1,
4917 1,
4918 0,
4919 ),
4920 ];
4921
4922 for (sql, before, after, fix_count) in cases {
4923 assert_rule_case(sql, issue_codes::LINT_ST_004, before, after, fix_count);
4924 }
4925 }
4926
4927 #[test]
4928 fn sqlfluff_cv003_cases_are_fixed_or_unchanged() {
4929 let cases = [
4930 ("SELECT a FROM foo WHERE a IS NULL", 0, 0, 0, None),
4931 ("SELECT a FROM foo WHERE a IS NOT NULL", 0, 0, 0, None),
4932 (
4933 "SELECT a FROM foo WHERE a <> NULL",
4934 1,
4935 0,
4936 1,
4937 Some("WHERE A IS NOT NULL"),
4938 ),
4939 (
4940 "SELECT a FROM foo WHERE a <> NULL AND b != NULL AND c = 'foo'",
4941 2,
4942 0,
4943 2,
4944 Some("A IS NOT NULL AND B IS NOT NULL"),
4945 ),
4946 (
4947 "SELECT a FROM foo WHERE a = NULL",
4948 1,
4949 0,
4950 1,
4951 Some("WHERE A IS NULL"),
4952 ),
4953 (
4954 "SELECT a FROM foo WHERE a=NULL",
4955 1,
4956 0,
4957 1,
4958 Some("WHERE A IS NULL"),
4959 ),
4960 (
4961 "SELECT a FROM foo WHERE a = b OR (c > d OR e = NULL)",
4962 1,
4963 0,
4964 1,
4965 Some("OR E IS NULL"),
4966 ),
4967 ("UPDATE table1 SET col = NULL WHERE col = ''", 0, 0, 0, None),
4968 ];
4969
4970 for (sql, before, after, fix_count, expected_text) in cases {
4971 assert_rule_case(sql, issue_codes::LINT_CV_005, before, after, fix_count);
4972
4973 if let Some(expected) = expected_text {
4974 let out = apply_lint_fixes(sql, Dialect::Generic, &[]).expect("fix result");
4975 assert!(
4976 out.sql.to_ascii_uppercase().contains(expected),
4977 "expected {expected:?} in fixed SQL, got: {}",
4978 out.sql
4979 );
4980 }
4981 }
4982 }
4983
4984 #[test]
4985 fn sqlfluff_cv001_cases_are_fixed_or_unchanged() {
4986 let cases = [
4987 ("SELECT coalesce(foo, 0) AS bar FROM baz", 0, 0, 0),
4988 ("SELECT ifnull(foo, 0) AS bar FROM baz", 1, 0, 1),
4989 ("SELECT nvl(foo, 0) AS bar FROM baz", 1, 0, 1),
4990 (
4991 "SELECT CASE WHEN x IS NULL THEN 'default' ELSE x END FROM t",
4992 0,
4993 0,
4994 0,
4995 ),
4996 ];
4997
4998 for (sql, before, after, fix_count) in cases {
4999 assert_rule_case(sql, issue_codes::LINT_CV_002, before, after, fix_count);
5000 }
5001 }
5002
5003 #[test]
5004 fn sqlfluff_cv003_trailing_comma_cases_are_fixed_or_unchanged() {
5005 let cases = [
5006 ("SELECT a, FROM t", 1, 0, 1),
5007 ("SELECT a , FROM t", 1, 0, 1),
5008 ("SELECT a FROM t", 0, 0, 0),
5009 ];
5010
5011 for (sql, before, after, fix_count) in cases {
5012 assert_rule_case(sql, issue_codes::LINT_CV_003, before, after, fix_count);
5013 }
5014 }
5015
5016 #[test]
5017 fn sqlfluff_cv001_not_equal_style_cases_are_fixed_or_unchanged() {
5018 let cases = [
5019 ("SELECT * FROM t WHERE a <> b AND c != d", 1, 0, 1),
5020 ("SELECT * FROM t WHERE a != b", 0, 0, 0),
5021 ];
5022
5023 for (sql, before, after, fix_count) in cases {
5024 assert_rule_case(sql, issue_codes::LINT_CV_001, before, after, fix_count);
5025 }
5026 }
5027
5028 #[test]
5029 fn sqlfluff_cv008_cases_are_fixed_or_unchanged() {
5030 let cases: [(&str, usize, usize, usize, Option<&str>); 4] = [
5031 ("SELECT * FROM a RIGHT JOIN b ON a.id = b.id", 1, 1, 0, None),
5032 (
5033 "SELECT a.id FROM a JOIN b ON a.id = b.id RIGHT JOIN c ON b.id = c.id",
5034 1,
5035 1,
5036 0,
5037 None,
5038 ),
5039 (
5040 "SELECT a.id FROM a RIGHT JOIN b ON a.id = b.id RIGHT JOIN c ON b.id = c.id",
5041 2,
5042 2,
5043 0,
5044 None,
5045 ),
5046 ("SELECT * FROM a LEFT JOIN b ON a.id = b.id", 0, 0, 0, None),
5047 ];
5048
5049 for (sql, before, after, fix_count, expected_text) in cases {
5050 assert_rule_case(sql, issue_codes::LINT_CV_008, before, after, fix_count);
5051
5052 if let Some(expected) = expected_text {
5053 let out = apply_lint_fixes(sql, Dialect::Generic, &[]).expect("fix result");
5054 assert!(
5055 out.sql.to_ascii_uppercase().contains(expected),
5056 "expected {expected:?} in fixed SQL, got: {}",
5057 out.sql
5058 );
5059 }
5060 }
5061 }
5062
5063 #[test]
5064 fn sqlfluff_cv007_cases_are_fixed_or_unchanged() {
5065 let cases = [
5066 ("(SELECT 1)", 1, 0, 1),
5067 ("((SELECT 1))", 1, 0, 1),
5068 ("SELECT 1", 0, 0, 0),
5069 ];
5070
5071 for (sql, before, after, fix_count) in cases {
5072 assert_rule_case(sql, issue_codes::LINT_CV_007, before, after, fix_count);
5073 }
5074 }
5075
5076 #[test]
5077 fn cv007_fix_respects_disabled_rules() {
5078 let sql = "(SELECT 1)\n";
5079 let out = apply_lint_fixes(
5080 sql,
5081 Dialect::Generic,
5082 &[issue_codes::LINT_CV_007.to_string()],
5083 )
5084 .expect("fix result");
5085 assert_eq!(out.sql, sql);
5086 assert_eq!(out.counts.get(issue_codes::LINT_CV_007), 0);
5087 }
5088
5089 #[test]
5090 fn cv010_fix_converts_double_to_single_in_bigquery() {
5091 let lint_config = LintConfig {
5092 enabled: true,
5093 disabled_rules: vec![],
5094 rule_configs: std::collections::BTreeMap::from([(
5095 "convention.quoted_literals".to_string(),
5096 serde_json::json!({"preferred_quoted_literal_style": "single_quotes"}),
5097 )]),
5098 };
5099 let sql = "SELECT \"abc\"";
5101 let before = lint_rule_count_with_config_in_dialect(
5102 sql,
5103 issue_codes::LINT_CV_010,
5104 Dialect::Bigquery,
5105 &lint_config,
5106 );
5107 assert_eq!(
5108 before, 1,
5109 "CV10 should flag double-quoted string in BigQuery with single_quotes preference"
5110 );
5111
5112 let out = apply_lint_fixes_with_lint_config(sql, Dialect::Bigquery, &lint_config)
5113 .expect("fix result");
5114 assert!(
5115 out.sql.contains("'abc'"),
5116 "expected double-quoted string to be converted to single-quoted: {}",
5117 out.sql
5118 );
5119 }
5120
5121 #[test]
5122 fn cv011_cast_preference_rewrites_double_colon_style() {
5123 let lint_config = LintConfig {
5124 enabled: true,
5125 disabled_rules: vec![],
5126 rule_configs: std::collections::BTreeMap::from([(
5127 "convention.casting_style".to_string(),
5128 serde_json::json!({"preferred_type_casting_style": "cast"}),
5129 )]),
5130 };
5131 let sql = "SELECT amount::INT FROM t";
5132 assert_rule_case_with_config(sql, issue_codes::LINT_CV_011, 1, 0, 1, &lint_config);
5133
5134 let out = apply_fix_with_config(sql, &lint_config);
5135 assert!(
5136 out.sql.to_ascii_uppercase().contains("CAST(AMOUNT AS INT)"),
5137 "expected CAST(...) rewrite for CV_011 fix: {}",
5138 out.sql
5139 );
5140 }
5141
5142 #[test]
5143 fn cv011_shorthand_preference_rewrites_cast_style_when_safe() {
5144 let lint_config = LintConfig {
5145 enabled: true,
5146 disabled_rules: vec![],
5147 rule_configs: std::collections::BTreeMap::from([(
5148 "LINT_CV_011".to_string(),
5149 serde_json::json!({"preferred_type_casting_style": "shorthand"}),
5150 )]),
5151 };
5152 let sql = "SELECT CAST(amount AS INT) FROM t";
5153 assert_rule_case_with_config(sql, issue_codes::LINT_CV_011, 1, 0, 1, &lint_config);
5154
5155 let out = apply_fix_with_config(sql, &lint_config);
5156 assert!(
5157 out.sql.to_ascii_uppercase().contains("AMOUNT::INT"),
5158 "expected :: rewrite for CV_011 fix: {}",
5159 out.sql
5160 );
5161 }
5162
5163 #[test]
5164 fn sqlfluff_st012_cases_are_fixed_or_unchanged() {
5165 let cases = [
5166 ("SELECT 1;;", 1, 0, 1),
5167 ("SELECT 1;\n \t ;", 1, 0, 1),
5168 ("SELECT 1;", 0, 0, 0),
5169 ];
5170
5171 for (sql, before, after, fix_count) in cases {
5172 assert_rule_case(sql, issue_codes::LINT_ST_012, before, after, fix_count);
5173 }
5174 }
5175
5176 #[test]
5177 fn sqlfluff_st002_cases_are_fixed_or_unchanged() {
5178 let cases = [
5179 ("SELECT CASE WHEN x > 1 THEN 'a' ELSE NULL END FROM t", 1, 0, 1),
5180 (
5181 "SELECT CASE name WHEN 'cat' THEN 'meow' WHEN 'dog' THEN 'woof' ELSE NULL END FROM t",
5182 1,
5183 0,
5184 1,
5185 ),
5186 (
5187 "SELECT CASE WHEN x = 1 THEN 'a' WHEN x = 2 THEN 'b' WHEN x = 3 THEN 'c' ELSE NULL END FROM t",
5188 1,
5189 0,
5190 1,
5191 ),
5192 (
5193 "SELECT CASE WHEN x > 0 THEN CASE WHEN y > 0 THEN 'pos' ELSE NULL END ELSE NULL END FROM t",
5194 2,
5195 0,
5196 2,
5197 ),
5198 (
5199 "SELECT * FROM t WHERE (CASE WHEN x > 0 THEN 1 ELSE NULL END) IS NOT NULL",
5200 1,
5201 0,
5202 1,
5203 ),
5204 (
5205 "WITH cte AS (SELECT CASE WHEN x > 0 THEN 'yes' ELSE NULL END AS flag FROM t) SELECT * FROM cte",
5206 1,
5207 0,
5208 1,
5209 ),
5210 ("SELECT CASE WHEN x > 1 THEN 'a' END FROM t", 0, 0, 0),
5211 (
5212 "SELECT CASE name WHEN 'cat' THEN 'meow' ELSE UPPER(name) END FROM t",
5213 0,
5214 0,
5215 0,
5216 ),
5217 ("SELECT CASE WHEN x > 1 THEN 'a' ELSE 'b' END FROM t", 0, 0, 0),
5218 ];
5219
5220 for (sql, before, after, fix_count) in cases {
5221 assert_rule_case(sql, issue_codes::LINT_ST_001, before, after, fix_count);
5222 }
5223 }
5224
5225 #[test]
5226 fn count_style_cases_are_fixed_or_unchanged() {
5227 let cases = [
5228 ("SELECT COUNT(1) FROM t", 1, 0, 1),
5229 (
5230 "SELECT col FROM t GROUP BY col HAVING COUNT(1) > 5",
5231 1,
5232 0,
5233 1,
5234 ),
5235 (
5236 "SELECT * FROM t WHERE id IN (SELECT COUNT(1) FROM t2 GROUP BY col)",
5237 1,
5238 0,
5239 1,
5240 ),
5241 ("SELECT COUNT(1), COUNT(1) FROM t", 2, 0, 2),
5242 (
5243 "WITH cte AS (SELECT COUNT(1) AS cnt FROM t) SELECT * FROM cte",
5244 1,
5245 0,
5246 1,
5247 ),
5248 ("SELECT COUNT(*) FROM t", 0, 0, 0),
5249 ("SELECT COUNT(id) FROM t", 0, 0, 0),
5250 ("SELECT COUNT(0) FROM t", 1, 0, 1),
5251 ("SELECT COUNT(01) FROM t", 1, 0, 1),
5252 ("SELECT COUNT(DISTINCT id) FROM t", 0, 0, 0),
5253 ];
5254
5255 for (sql, before, after, fix_count) in cases {
5256 assert_rule_case(sql, issue_codes::LINT_CV_004, before, after, fix_count);
5257 }
5258 }
5259
5260 #[test]
5261 fn safe_mode_blocks_template_tag_edits_but_applies_non_template_fixes() {
5262 let sql = "SELECT '{{foo}}' AS templated, COUNT(1) FROM t";
5263 let out = apply_lint_fixes_with_options(
5264 sql,
5265 Dialect::Generic,
5266 &default_lint_config(),
5267 FixOptions {
5268 include_unsafe_fixes: false,
5269 include_rewrite_candidates: true,
5270 },
5271 )
5272 .expect("fix result");
5273
5274 assert!(
5275 out.sql.contains("{{foo}}"),
5276 "template tag bytes should be preserved in safe mode: {}",
5277 out.sql
5278 );
5279 assert!(
5280 out.sql.to_ascii_uppercase().contains("COUNT(*)"),
5281 "non-template safe fixes should still apply: {}",
5282 out.sql
5283 );
5284 assert!(
5285 out.skipped_counts.protected_range_blocked > 0,
5286 "template-tag edits should be blocked in safe mode"
5287 );
5288 }
5289
5290 #[test]
5291 fn unsafe_mode_allows_template_tag_edits() {
5292 let sql = "SELECT '{{foo}}' AS templated, COUNT(1) FROM t";
5293 let out = apply_lint_fixes_with_options(
5294 sql,
5295 Dialect::Generic,
5296 &default_lint_config(),
5297 FixOptions {
5298 include_unsafe_fixes: true,
5299 include_rewrite_candidates: false,
5300 },
5301 )
5302 .expect("fix result");
5303
5304 assert!(
5305 out.sql.contains("{{ foo }}"),
5306 "unsafe mode should allow template-tag formatting fixes: {}",
5307 out.sql
5308 );
5309 assert!(
5310 out.sql.to_ascii_uppercase().contains("COUNT(*)"),
5311 "other fixes should still apply: {}",
5312 out.sql
5313 );
5314 }
5315
5316 #[test]
5317 fn comments_are_not_globally_skipped() {
5318 let sql = "-- keep this comment\nSELECT COUNT(1) FROM t";
5319 let out = apply_lint_fixes_with_options(
5320 sql,
5321 Dialect::Generic,
5322 &default_lint_config(),
5323 FixOptions {
5324 include_unsafe_fixes: false,
5325 include_rewrite_candidates: false,
5326 },
5327 )
5328 .expect("fix result");
5329 assert!(
5330 !out.skipped_due_to_comments,
5331 "comment presence should not skip all fixes"
5332 );
5333 assert!(
5334 out.sql.contains("-- keep this comment"),
5335 "comment text must be preserved: {}",
5336 out.sql
5337 );
5338 assert!(
5339 out.sql.to_ascii_uppercase().contains("COUNT(*)"),
5340 "non-comment region should still be fixable: {}",
5341 out.sql
5342 );
5343 }
5344
5345 #[test]
5346 fn mysql_hash_comments_are_not_globally_skipped() {
5347 let sql = "# keep this comment\nSELECT COUNT(1) FROM t";
5348 let out = apply_lint_fixes_with_options(
5349 sql,
5350 Dialect::Mysql,
5351 &default_lint_config(),
5352 FixOptions {
5353 include_unsafe_fixes: false,
5354 include_rewrite_candidates: false,
5355 },
5356 )
5357 .expect("fix result");
5358 assert!(
5359 !out.skipped_due_to_comments,
5360 "comment presence should not skip all fixes"
5361 );
5362 assert!(
5363 out.sql.contains("# keep this comment"),
5364 "comment text must be preserved: {}",
5365 out.sql
5366 );
5367 assert!(
5368 out.sql.to_ascii_uppercase().contains("COUNT(*)"),
5369 "non-comment region should still be fixable: {}",
5370 out.sql
5371 );
5372 }
5373
5374 #[test]
5375 fn does_not_treat_double_quoted_comment_markers_as_comments() {
5376 let sql = "SELECT \"a--b\", \"x/*y\" FROM t";
5377 assert!(!contains_comment_markers(sql, Dialect::Generic));
5378 }
5379
5380 #[test]
5381 fn does_not_treat_backtick_or_bracketed_markers_as_comments() {
5382 let sql = "SELECT `a--b`, [x/*y] FROM t";
5383 assert!(!contains_comment_markers(sql, Dialect::Mysql));
5384 }
5385
5386 #[test]
5387 fn fix_mode_does_not_skip_double_quoted_markers() {
5388 let sql = "SELECT \"a--b\", COUNT(1) FROM t";
5389 let out = apply_lint_fixes(sql, Dialect::Generic, &[]).expect("fix result");
5390 assert!(!out.skipped_due_to_comments);
5391 }
5392
5393 #[test]
5394 fn fix_mode_does_not_skip_backtick_markers() {
5395 let sql = "SELECT `a--b`, COUNT(1) FROM t";
5396 let out = apply_lint_fixes(sql, Dialect::Mysql, &[]).expect("fix result");
5397 assert!(!out.skipped_due_to_comments);
5398 }
5399
5400 #[test]
5401 fn planner_blocks_protected_ranges_and_applies_non_overlapping_edits() {
5402 let sql = "SELECT '{{foo}}' AS templated, 1";
5403 let protected = collect_comment_protected_ranges(sql, Dialect::Generic, true);
5404 let template_idx = sql.find("{{foo}}").expect("template exists");
5405 let one_idx = sql.rfind('1').expect("digit exists");
5406
5407 let planned = plan_fix_candidates(
5408 sql,
5409 vec![
5410 FixCandidate {
5411 start: template_idx,
5412 end: template_idx + "{{foo}}".len(),
5413 replacement: String::new(),
5414 applicability: FixCandidateApplicability::Safe,
5415 source: FixCandidateSource::PrimaryRewrite,
5416 rule_code: None,
5417 },
5418 FixCandidate {
5419 start: one_idx,
5420 end: one_idx + 1,
5421 replacement: "2".to_string(),
5422 applicability: FixCandidateApplicability::Safe,
5423 source: FixCandidateSource::PrimaryRewrite,
5424 rule_code: None,
5425 },
5426 ],
5427 &protected,
5428 false,
5429 );
5430
5431 let applied = apply_planned_edits(sql, &planned.edits);
5432 assert!(
5433 applied.contains("{{foo}}"),
5434 "template span should remain protected: {applied}"
5435 );
5436 assert!(
5437 applied.ends_with("2"),
5438 "expected non-overlapping edit: {applied}"
5439 );
5440 assert_eq!(planned.skipped.protected_range_blocked, 1);
5441 }
5442
5443 #[test]
5444 fn planner_is_deterministic_for_conflicting_candidates() {
5445 let sql = "SELECT 1";
5446 let one_idx = sql.rfind('1').expect("digit exists");
5447
5448 let left_first = plan_fix_candidates(
5449 sql,
5450 vec![
5451 FixCandidate {
5452 start: one_idx,
5453 end: one_idx + 1,
5454 replacement: "9".to_string(),
5455 applicability: FixCandidateApplicability::Safe,
5456 source: FixCandidateSource::PrimaryRewrite,
5457 rule_code: None,
5458 },
5459 FixCandidate {
5460 start: one_idx,
5461 end: one_idx + 1,
5462 replacement: "2".to_string(),
5463 applicability: FixCandidateApplicability::Safe,
5464 source: FixCandidateSource::PrimaryRewrite,
5465 rule_code: None,
5466 },
5467 ],
5468 &[],
5469 false,
5470 );
5471 let right_first = plan_fix_candidates(
5472 sql,
5473 vec![
5474 FixCandidate {
5475 start: one_idx,
5476 end: one_idx + 1,
5477 replacement: "2".to_string(),
5478 applicability: FixCandidateApplicability::Safe,
5479 source: FixCandidateSource::PrimaryRewrite,
5480 rule_code: None,
5481 },
5482 FixCandidate {
5483 start: one_idx,
5484 end: one_idx + 1,
5485 replacement: "9".to_string(),
5486 applicability: FixCandidateApplicability::Safe,
5487 source: FixCandidateSource::PrimaryRewrite,
5488 rule_code: None,
5489 },
5490 ],
5491 &[],
5492 false,
5493 );
5494
5495 let left_sql = apply_planned_edits(sql, &left_first.edits);
5496 let right_sql = apply_planned_edits(sql, &right_first.edits);
5497 assert_eq!(left_sql, "SELECT 2");
5498 assert_eq!(left_sql, right_sql);
5499 assert_eq!(left_first.skipped.overlap_conflict_blocked, 1);
5500 assert_eq!(right_first.skipped.overlap_conflict_blocked, 1);
5501 }
5502
5503 #[test]
5504 fn core_autofix_candidates_are_collected_and_applied() {
5505 let sql = "SELECT 1";
5506 let one_idx = sql.rfind('1').expect("digit exists");
5507 let issues = vec![serde_json::json!({
5508 "code": issue_codes::LINT_CV_004,
5509 "span": { "start": one_idx, "end": one_idx + 1 },
5510 "autofix": {
5511 "applicability": "safe",
5512 "edits": [
5513 {
5514 "start": one_idx,
5515 "end": one_idx + 1,
5516 "replacement": "2"
5517 }
5518 ]
5519 }
5520 })];
5521 let candidates = build_fix_candidates_from_issue_values(sql, &issues);
5522
5523 assert_eq!(candidates.len(), 1);
5524 let planned = plan_fix_candidates(sql, candidates, &[], false);
5525 let applied = apply_planned_edits(sql, &planned.edits);
5526 assert_eq!(applied, "SELECT 2");
5527 }
5528
5529 #[test]
5530 fn st002_core_autofix_candidates_apply_cleanly_in_safe_mode() {
5531 let sql = "SELECT CASE WHEN x > 0 THEN true ELSE false END FROM t\n";
5532 let issues = lint_issues(sql, Dialect::Generic, &default_lint_config());
5533 let candidates = build_fix_candidates_from_issue_autofixes(sql, &issues);
5534 assert!(
5535 candidates
5536 .iter()
5537 .any(|candidate| candidate.rule_code.as_deref() == Some(issue_codes::LINT_ST_002)),
5538 "expected ST002 core candidate from lint issues: {candidates:?}"
5539 );
5540
5541 let protected = collect_comment_protected_ranges(sql, Dialect::Generic, true);
5542 let planned = plan_fix_candidates(sql, candidates, &protected, false);
5543 let applied = apply_planned_edits(sql, &planned.edits);
5544 assert_eq!(
5545 applied, "SELECT coalesce(x > 0, false) FROM t\n",
5546 "unexpected ST002 planned edits with skipped={:?}",
5547 planned.skipped
5548 );
5549 }
5550
5551 #[test]
5552 fn incremental_core_plan_applies_st009_even_when_not_top_priority() {
5553 let sql = "select foo.a, bar.b from foo left join bar on bar.a = foo.a";
5554 let lint_config = default_lint_config();
5555 let before_counts = lint_rule_counts(sql, Dialect::Generic, &lint_config);
5556 assert_eq!(
5557 before_counts
5558 .get(issue_codes::LINT_ST_009)
5559 .copied()
5560 .unwrap_or(0),
5561 1
5562 );
5563
5564 let out = try_incremental_core_fix_plan(
5565 sql,
5566 Dialect::Generic,
5567 &lint_config,
5568 &before_counts,
5569 None,
5570 false,
5571 24,
5572 usize::MAX,
5573 )
5574 .expect("expected incremental ST009 fix");
5575 assert!(
5576 out.sql.contains("foo.a = bar.a"),
5577 "expected ST009 join condition reorder, got: {}",
5578 out.sql
5579 );
5580
5581 let after_counts = lint_rule_counts(&out.sql, Dialect::Generic, &lint_config);
5582 assert_eq!(
5583 after_counts
5584 .get(issue_codes::LINT_ST_009)
5585 .copied()
5586 .unwrap_or(0),
5587 0
5588 );
5589 }
5590
5591 #[test]
5592 fn cached_pre_lint_state_matches_uncached_next_pass_behavior() {
5593 let sql = "SELECT 1 UNION SELECT 2";
5594 let lint_config = default_lint_config();
5595 let fix_options = FixOptions {
5596 include_unsafe_fixes: false,
5597 include_rewrite_candidates: false,
5598 };
5599
5600 let first_pass = apply_lint_fixes_with_options_and_lint_state(
5601 sql,
5602 Dialect::Generic,
5603 &lint_config,
5604 fix_options,
5605 None,
5606 )
5607 .expect("first fix pass");
5608
5609 let second_cached = apply_lint_fixes_with_options_and_lint_state(
5610 &first_pass.outcome.sql,
5611 Dialect::Generic,
5612 &lint_config,
5613 fix_options,
5614 Some(first_pass.post_lint_state.clone()),
5615 )
5616 .expect("second cached pass");
5617 let second_uncached = apply_lint_fixes_with_options_and_lint_state(
5618 &first_pass.outcome.sql,
5619 Dialect::Generic,
5620 &lint_config,
5621 fix_options,
5622 None,
5623 )
5624 .expect("second uncached pass");
5625
5626 assert_eq!(second_cached.outcome.sql, second_uncached.outcome.sql);
5627 assert_eq!(second_cached.outcome.counts, second_uncached.outcome.counts);
5628 assert_eq!(
5629 second_cached.outcome.changed,
5630 second_uncached.outcome.changed
5631 );
5632 assert_eq!(
5633 second_cached.outcome.skipped_due_to_regression,
5634 second_uncached.outcome.skipped_due_to_regression
5635 );
5636 }
5637
5638 #[test]
5639 fn cp03_templated_case_emits_core_autofix_candidate() {
5640 let sql = "SELECT\n {{ \"greatest(a, b)\" }},\n GREATEST(i, j)\n";
5641 let config = lint_config_keep_only_rule(
5642 issue_codes::LINT_CP_003,
5643 LintConfig {
5644 enabled: true,
5645 disabled_rules: vec![],
5646 rule_configs: std::collections::BTreeMap::from([(
5647 "core".to_string(),
5648 serde_json::json!({"ignore_templated_areas": false}),
5649 )]),
5650 },
5651 );
5652 let issues = lint_issues(sql, Dialect::Ansi, &config);
5653 assert!(
5654 issues
5655 .iter()
5656 .any(|issue| { issue.code == issue_codes::LINT_CP_003 && issue.autofix.is_some() }),
5657 "expected CP03 issue with autofix metadata, got issues={issues:?}"
5658 );
5659
5660 let candidates = build_fix_candidates_from_issue_autofixes(sql, &issues);
5661 assert!(
5662 candidates.iter().any(|candidate| {
5663 candidate.rule_code.as_deref() == Some(issue_codes::LINT_CP_003)
5664 && &sql[candidate.start..candidate.end] == "GREATEST"
5665 && candidate.replacement == "greatest"
5666 }),
5667 "expected CP03 GREATEST candidate, got candidates={candidates:?}"
5668 );
5669 }
5670
5671 #[test]
5672 fn planner_prefers_core_autofix_over_rewrite_conflicts() {
5673 let sql = "SELECT 1";
5674 let one_idx = sql.rfind('1').expect("digit exists");
5675 let core_issue = serde_json::json!({
5676 "code": issue_codes::LINT_CV_004,
5677 "autofix": {
5678 "start": one_idx,
5679 "end": one_idx + 1,
5680 "replacement": "9",
5681 "applicability": "safe"
5682 }
5683 });
5684 let core_candidate = build_fix_candidates_from_issue_values(sql, &[core_issue])[0].clone();
5685 let rewrite_candidate = FixCandidate {
5686 start: one_idx,
5687 end: one_idx + 1,
5688 replacement: "2".to_string(),
5689 applicability: FixCandidateApplicability::Safe,
5690 source: FixCandidateSource::PrimaryRewrite,
5691 rule_code: None,
5692 };
5693
5694 let left_first = plan_fix_candidates(
5695 sql,
5696 vec![rewrite_candidate.clone(), core_candidate.clone()],
5697 &[],
5698 false,
5699 );
5700 let right_first =
5701 plan_fix_candidates(sql, vec![core_candidate, rewrite_candidate], &[], false);
5702
5703 let left_sql = apply_planned_edits(sql, &left_first.edits);
5704 let right_sql = apply_planned_edits(sql, &right_first.edits);
5705 assert_eq!(left_sql, "SELECT 9");
5706 assert_eq!(left_sql, right_sql);
5707 assert_eq!(left_first.skipped.overlap_conflict_blocked, 1);
5708 assert_eq!(right_first.skipped.overlap_conflict_blocked, 1);
5709 }
5710
5711 #[test]
5712 fn rewrite_mode_falls_back_to_core_plan_when_core_rule_is_not_improved() {
5713 let sql = "SELECT * FROM t WHERE a <> b AND c != d";
5716 let out = apply_lint_fixes_with_options(
5717 sql,
5718 Dialect::Generic,
5719 &default_lint_config(),
5720 FixOptions {
5721 include_unsafe_fixes: true,
5722 include_rewrite_candidates: true,
5723 },
5724 )
5725 .expect("fix result");
5726
5727 assert_eq!(fix_count_for_code(&out.counts, issue_codes::LINT_CV_001), 1);
5728 assert!(
5729 out.sql.contains("a <> b"),
5730 "expected CV001 style fix: {}",
5731 out.sql
5732 );
5733 assert!(
5734 out.sql.contains("c <> d"),
5735 "expected CV001 style fix: {}",
5736 out.sql
5737 );
5738 assert!(
5739 !out.sql.contains("!="),
5740 "expected no bang-style operator: {}",
5741 out.sql
5742 );
5743 }
5744
5745 #[test]
5746 fn core_autofix_applicability_is_mapped_to_existing_planner_logic() {
5747 let sql = "SELECT 1";
5748 let one_idx = sql.rfind('1').expect("digit exists");
5749 let issues = vec![
5750 serde_json::json!({
5751 "code": issue_codes::LINT_ST_005,
5752 "autofix": {
5753 "start": one_idx,
5754 "end": one_idx + 1,
5755 "replacement": "2",
5756 "applicability": "unsafe"
5757 }
5758 }),
5759 serde_json::json!({
5760 "code": issue_codes::LINT_ST_005,
5761 "autofix": {
5762 "start": one_idx,
5763 "end": one_idx + 1,
5764 "replacement": "3",
5765 "applicability": "display_only"
5766 }
5767 }),
5768 ];
5769 let candidates = build_fix_candidates_from_issue_values(sql, &issues);
5770
5771 assert_eq!(
5772 candidates[0].applicability,
5773 FixCandidateApplicability::Unsafe
5774 );
5775 assert_eq!(
5776 candidates[1].applicability,
5777 FixCandidateApplicability::DisplayOnly
5778 );
5779
5780 let planned_safe = plan_fix_candidates(sql, candidates.clone(), &[], false);
5781 assert_eq!(apply_planned_edits(sql, &planned_safe.edits), sql);
5782 assert_eq!(planned_safe.skipped.unsafe_skipped, 1);
5783 assert_eq!(planned_safe.skipped.display_only, 1);
5784
5785 let planned_unsafe = plan_fix_candidates(sql, candidates, &[], true);
5786 assert_eq!(apply_planned_edits(sql, &planned_unsafe.edits), "SELECT 2");
5787 assert_eq!(planned_unsafe.skipped.display_only, 1);
5788 }
5789
5790 #[test]
5791 fn planner_tracks_unsafe_and_display_only_skips() {
5792 let sql = "SELECT 1";
5793 let one_idx = sql.rfind('1').expect("digit exists");
5794 let planned = plan_fix_candidates(
5795 sql,
5796 vec![
5797 FixCandidate {
5798 start: one_idx,
5799 end: one_idx + 1,
5800 replacement: "2".to_string(),
5801 applicability: FixCandidateApplicability::Unsafe,
5802 source: FixCandidateSource::UnsafeFallback,
5803 rule_code: None,
5804 },
5805 FixCandidate {
5806 start: 0,
5807 end: 0,
5808 replacement: String::new(),
5809 applicability: FixCandidateApplicability::DisplayOnly,
5810 source: FixCandidateSource::DisplayHint,
5811 rule_code: None,
5812 },
5813 ],
5814 &[],
5815 false,
5816 );
5817 let applied = apply_planned_edits(sql, &planned.edits);
5818 assert_eq!(applied, sql);
5819 assert_eq!(planned.skipped.unsafe_skipped, 1);
5820 assert_eq!(planned.skipped.display_only, 1);
5821 }
5822
5823 #[test]
5824 fn does_not_collapse_independent_select_statements() {
5825 let sql = "SELECT 1; SELECT 2;";
5826 let out = apply_lint_fixes(sql, Dialect::Generic, &[]).expect("fix result");
5827 assert!(
5828 !out.sql.to_ascii_uppercase().contains("DISTINCT SELECT"),
5829 "auto-fix must preserve statement boundaries: {}",
5830 out.sql
5831 );
5832 let parsed = parse_sql_with_dialect(&out.sql, Dialect::Generic).expect("parse fixed sql");
5833 assert_eq!(
5834 parsed.len(),
5835 2,
5836 "auto-fix should preserve two independent statements"
5837 );
5838 }
5839
5840 #[test]
5841 fn subquery_to_cte_text_fix_applies() {
5842 let fixed = fix_subquery_to_cte("SELECT * FROM (SELECT 1) sub");
5843 assert_eq!(fixed, "WITH sub AS (SELECT 1) SELECT * FROM sub");
5844 }
5845
5846 #[test]
5847 fn st005_core_autofix_applies_in_unsafe_mode_with_from_config() {
5848 let sql = "SELECT * FROM (SELECT 1) sub";
5849 let lint_config = LintConfig {
5850 enabled: true,
5851 disabled_rules: vec![],
5852 rule_configs: std::collections::BTreeMap::from([(
5853 "structure.subquery".to_string(),
5854 serde_json::json!({"forbid_subquery_in": "from"}),
5855 )]),
5856 };
5857
5858 let fixed = apply_lint_fixes_with_options(
5859 sql,
5860 Dialect::Generic,
5861 &lint_config,
5862 FixOptions {
5863 include_unsafe_fixes: true,
5864 include_rewrite_candidates: false,
5865 },
5866 )
5867 .expect("fix result")
5868 .sql;
5869 assert!(
5870 fixed.to_ascii_uppercase().contains("WITH SUB AS"),
5871 "expected unsafe core ST005 autofix to rewrite to CTE, got: {fixed}"
5872 );
5873 }
5874
5875 #[test]
5876 fn subquery_to_cte_text_fix_handles_nested_parentheses() {
5877 let fixed = fix_subquery_to_cte("SELECT * FROM (SELECT COUNT(*) FROM t) sub");
5878 assert_eq!(
5879 fixed,
5880 "WITH sub AS (SELECT COUNT(*) FROM t) SELECT * FROM sub"
5881 );
5882 parse_sql_with_dialect(&fixed, Dialect::Generic).expect("fixed SQL should parse");
5883 }
5884
5885 #[test]
5886 fn st005_ast_fix_rewrites_simple_join_derived_subquery_to_cte() {
5887 let lint_config = LintConfig {
5888 enabled: true,
5889 disabled_rules: vec![issue_codes::LINT_AM_005.to_string()],
5890 rule_configs: std::collections::BTreeMap::new(),
5891 };
5892 let sql = "SELECT t.id FROM t JOIN (SELECT id FROM u) sub ON t.id = sub.id";
5893 assert_rule_case_with_config(sql, issue_codes::LINT_ST_005, 1, 0, 1, &lint_config);
5894
5895 let out = apply_fix_with_config(sql, &lint_config);
5896 assert!(
5897 out.sql.to_ascii_uppercase().contains("WITH SUB AS"),
5898 "expected AST ST_005 rewrite to emit CTE: {}",
5899 out.sql
5900 );
5901 }
5902
5903 #[test]
5904 fn st005_ast_fix_rewrites_simple_from_derived_subquery_with_config() {
5905 let lint_config = LintConfig {
5906 enabled: true,
5907 disabled_rules: vec![],
5908 rule_configs: std::collections::BTreeMap::from([(
5909 "structure.subquery".to_string(),
5910 serde_json::json!({"forbid_subquery_in": "from"}),
5911 )]),
5912 };
5913 let sql = "SELECT sub.id FROM (SELECT id FROM u) sub";
5914 assert_rule_case_with_config(sql, issue_codes::LINT_ST_005, 1, 0, 1, &lint_config);
5915
5916 let out = apply_fix_with_config(sql, &lint_config);
5917 assert!(
5918 out.sql.to_ascii_uppercase().contains("WITH SUB AS"),
5919 "expected FROM-derived ST_005 rewrite to emit CTE: {}",
5920 out.sql
5921 );
5922 }
5923
5924 #[test]
5925 fn consecutive_semicolon_fix_ignores_string_literal_content() {
5926 let sql = "SELECT 'a;;b' AS txt;;";
5927 let out = apply_lint_fixes_with_options(
5928 sql,
5929 Dialect::Generic,
5930 &default_lint_config(),
5931 FixOptions {
5932 include_unsafe_fixes: true,
5933 include_rewrite_candidates: false,
5934 },
5935 )
5936 .expect("fix result");
5937 assert!(
5938 out.sql.contains("'a;;b'"),
5939 "string literal content must be preserved: {}",
5940 out.sql
5941 );
5942 assert!(
5943 out.sql.trim_end().ends_with(';') && !out.sql.trim_end().ends_with(";;"),
5944 "trailing semicolon run should be collapsed to one terminator: {}",
5945 out.sql
5946 );
5947 }
5948
5949 #[test]
5950 fn consecutive_semicolon_fix_collapses_whitespace_separated_runs() {
5951 let out = apply_lint_fixes_with_options(
5952 "SELECT 1;\n \t ;",
5953 Dialect::Generic,
5954 &default_lint_config(),
5955 FixOptions {
5956 include_unsafe_fixes: true,
5957 include_rewrite_candidates: false,
5958 },
5959 )
5960 .expect("fix result");
5961 assert_eq!(out.sql.matches(';').count(), 1);
5962 }
5963
5964 #[test]
5965 fn lint_fix_subquery_with_function_call_is_parseable() {
5966 let sql = "SELECT * FROM (SELECT COUNT(*) FROM t) sub";
5967 let out = apply_lint_fixes(sql, Dialect::Generic, &[]).expect("fix result");
5968 assert!(
5969 !out.skipped_due_to_regression,
5970 "function-call subquery rewrite should not be treated as regression: {}",
5971 out.sql
5972 );
5973 parse_sql_with_dialect(&out.sql, Dialect::Generic).expect("fixed SQL should parse");
5974 }
5975
5976 #[test]
5977 fn distinct_parentheses_fix_preserves_valid_sql() {
5978 let sql = "SELECT DISTINCT(a) FROM t";
5979 let out = apply_lint_fixes(sql, Dialect::Generic, &[]).expect("fix result");
5980 assert!(
5981 !out.sql.contains("a)"),
5982 "unexpected dangling parenthesis after fix: {}",
5983 out.sql
5984 );
5985 parse_sql_with_dialect(&out.sql, Dialect::Generic).expect("fixed SQL should parse");
5986 }
5987
5988 #[test]
5989 fn not_equal_fix_does_not_rewrite_string_literals() {
5990 let sql = "SELECT '<>' AS x, a<>b, c!=d FROM t";
5991 let out = apply_lint_fixes_with_options(
5992 sql,
5993 Dialect::Generic,
5994 &default_lint_config(),
5995 FixOptions {
5996 include_unsafe_fixes: false,
5997 include_rewrite_candidates: false,
5998 },
5999 )
6000 .expect("fix result");
6001 assert!(
6002 out.sql.contains("'<>'"),
6003 "string literal should remain unchanged: {}",
6004 out.sql
6005 );
6006 let compact: String = out.sql.chars().filter(|ch| !ch.is_whitespace()).collect();
6007 let has_c_style = compact.contains("a!=b") && compact.contains("c!=d");
6008 let has_ansi_style = compact.contains("a<>b") && compact.contains("c<>d");
6009 assert!(
6010 has_c_style || has_ansi_style || compact.contains("a<>b") && compact.contains("c!=d"),
6011 "operator usage outside string literals should remain intact: {}",
6012 out.sql
6013 );
6014 }
6015
6016 #[test]
6017 fn spacing_fixes_do_not_rewrite_single_quoted_literals() {
6018 let operator_fixed = apply_lint_fixes_with_options(
6019 "SELECT payload->>'id', 'x=y' FROM t",
6020 Dialect::Generic,
6021 &default_lint_config(),
6022 FixOptions {
6023 include_unsafe_fixes: false,
6024 include_rewrite_candidates: false,
6025 },
6026 )
6027 .expect("operator spacing fix result")
6028 .sql;
6029 assert!(
6030 operator_fixed.contains("'x=y'"),
6031 "operator spacing must not mutate literals: {operator_fixed}"
6032 );
6033 assert!(
6034 operator_fixed.contains("payload ->>"),
6035 "operator spacing should still apply: {operator_fixed}"
6036 );
6037
6038 let comma_fixed = apply_lint_fixes_with_options(
6039 "SELECT a,b, 'x,y' FROM t",
6040 Dialect::Generic,
6041 &default_lint_config(),
6042 FixOptions {
6043 include_unsafe_fixes: false,
6044 include_rewrite_candidates: false,
6045 },
6046 )
6047 .expect("comma spacing fix result")
6048 .sql;
6049 assert!(
6050 comma_fixed.contains("'x,y'"),
6051 "comma spacing must not mutate literals: {comma_fixed}"
6052 );
6053 assert!(
6054 !comma_fixed.contains("a,b"),
6055 "comma spacing should still apply: {comma_fixed}"
6056 );
6057 }
6058
6059 #[test]
6060 fn keyword_newline_fix_does_not_rewrite_literals_or_quoted_identifiers() {
6061 let sql = "SELECT COUNT(1), 'hello FROM world', \"x WHERE y\" FROM t WHERE a = 1";
6062 let fixed = apply_lint_fixes(sql, Dialect::Generic, &[])
6063 .expect("fix result")
6064 .sql;
6065 assert!(
6066 fixed.contains("'hello FROM world'"),
6067 "single-quoted literal should remain unchanged: {fixed}"
6068 );
6069 assert!(
6070 fixed.contains("\"x WHERE y\""),
6071 "double-quoted identifier should remain unchanged: {fixed}"
6072 );
6073 assert!(
6074 !fixed.contains("hello\nFROM world"),
6075 "keyword newline fix must not inject newlines into literals: {fixed}"
6076 );
6077 }
6078
6079 #[test]
6080 fn cp04_fix_reduces_literal_capitalisation_violations() {
6081 assert_rule_case(
6083 "SELECT NULL, true, False FROM t",
6084 issue_codes::LINT_CP_004,
6085 2,
6086 0,
6087 2,
6088 );
6089 }
6090
6091 #[test]
6092 fn cp05_fix_reduces_type_capitalisation_violations() {
6093 assert_rule_case(
6095 "CREATE TABLE t (a INT, b VarChar(10));",
6096 issue_codes::LINT_CP_005,
6097 1,
6098 0,
6099 1,
6100 );
6101 }
6102
6103 #[test]
6104 fn cv06_fix_adds_missing_final_terminator() {
6105 assert_rule_case("SELECT 1 ;", issue_codes::LINT_CV_006, 1, 0, 1);
6106 }
6107
6108 #[test]
6109 fn lt03_fix_moves_trailing_operator_to_leading_position() {
6110 assert_rule_case("SELECT a +\n b FROM t", issue_codes::LINT_LT_003, 1, 0, 1);
6111 }
6112
6113 #[test]
6114 fn lt04_fix_moves_comma_around_templated_columns_in_ansi() {
6115 let leading_sql = "SELECT\n c1,\n {{ \"c2\" }} AS days_since\nFROM logs";
6116 let leading_config = lint_config_keep_only_rule(
6117 issue_codes::LINT_LT_004,
6118 LintConfig {
6119 enabled: true,
6120 disabled_rules: vec![],
6121 rule_configs: std::collections::BTreeMap::from([(
6122 "layout.commas".to_string(),
6123 serde_json::json!({"line_position": "leading"}),
6124 )]),
6125 },
6126 );
6127 let leading_issues = lint_issues(leading_sql, Dialect::Ansi, &leading_config);
6128 let leading_lt04 = leading_issues
6129 .iter()
6130 .find(|issue| issue.code == issue_codes::LINT_LT_004)
6131 .expect("expected LT04 issue before fix");
6132 assert!(
6133 leading_lt04.autofix.is_some(),
6134 "expected LT04 issue to carry autofix metadata in fix pipeline"
6135 );
6136 let leading_out = apply_lint_fixes_with_options(
6137 leading_sql,
6138 Dialect::Ansi,
6139 &leading_config,
6140 FixOptions {
6141 include_unsafe_fixes: true,
6142 include_rewrite_candidates: false,
6143 },
6144 )
6145 .expect("fix result");
6146 assert!(
6147 !leading_out.skipped_due_to_regression,
6148 "LT04 leading templated fix should not be treated as regression"
6149 );
6150 assert_eq!(
6151 leading_out.sql,
6152 "SELECT\n c1\n , {{ \"c2\" }} AS days_since\nFROM logs"
6153 );
6154
6155 let trailing_sql = "SELECT\n {{ \"c1\" }}\n , c2 AS days_since\nFROM logs";
6156 let trailing_config =
6157 lint_config_keep_only_rule(issue_codes::LINT_LT_004, default_lint_config());
6158 let trailing_out = apply_lint_fixes_with_options(
6159 trailing_sql,
6160 Dialect::Ansi,
6161 &trailing_config,
6162 FixOptions {
6163 include_unsafe_fixes: true,
6164 include_rewrite_candidates: false,
6165 },
6166 )
6167 .expect("fix result");
6168 assert!(
6169 !trailing_out.skipped_due_to_regression,
6170 "LT04 trailing templated fix should not be treated as regression"
6171 );
6172 assert_eq!(
6173 trailing_out.sql,
6174 "SELECT\n {{ \"c1\" }},\n c2 AS days_since\nFROM logs"
6175 );
6176 }
6177 #[test]
6178 fn rf004_core_autofix_respects_rule_filter() {
6179 let sql = "select a from users as select\n";
6180
6181 let out_rf_disabled = apply_lint_fixes(
6182 sql,
6183 Dialect::Generic,
6184 &[issue_codes::LINT_RF_004.to_string()],
6185 )
6186 .expect("fix result");
6187 assert_eq!(
6188 out_rf_disabled.sql, sql,
6189 "excluding RF_004 should block alias-keyword core autofix"
6190 );
6191
6192 let out_al_disabled = apply_lint_fixes(
6193 sql,
6194 Dialect::Generic,
6195 &[issue_codes::LINT_AL_005.to_string()],
6196 )
6197 .expect("fix result");
6198 assert!(
6199 out_al_disabled.sql.contains("alias_select"),
6200 "excluding AL_005 must not block RF_004 core autofix: {}",
6201 out_al_disabled.sql
6202 );
6203 }
6204
6205 #[test]
6206 fn rf003_core_autofix_respects_rule_filter() {
6207 let sql = "select a.id, id2 from a\n";
6208
6209 let rf_disabled_config = LintConfig {
6210 enabled: true,
6211 disabled_rules: vec![issue_codes::LINT_RF_003.to_string()],
6212 rule_configs: std::collections::BTreeMap::new(),
6213 };
6214 let out_rf_disabled = apply_lint_fixes_with_options(
6215 sql,
6216 Dialect::Generic,
6217 &rf_disabled_config,
6218 FixOptions {
6219 include_unsafe_fixes: true,
6220 include_rewrite_candidates: false,
6221 },
6222 )
6223 .expect("fix result");
6224 assert!(
6225 !out_rf_disabled.sql.contains("a.id2"),
6226 "excluding RF_003 should block reference qualification core autofix: {}",
6227 out_rf_disabled.sql
6228 );
6229
6230 let al_disabled_config = LintConfig {
6231 enabled: true,
6232 disabled_rules: vec![issue_codes::LINT_AL_005.to_string()],
6233 rule_configs: std::collections::BTreeMap::new(),
6234 };
6235 let out_al_disabled = apply_lint_fixes_with_options(
6236 sql,
6237 Dialect::Generic,
6238 &al_disabled_config,
6239 FixOptions {
6240 include_unsafe_fixes: true,
6241 include_rewrite_candidates: false,
6242 },
6243 )
6244 .expect("fix result");
6245 assert!(
6246 out_al_disabled.sql.contains("a.id2"),
6247 "excluding AL_005 must not block RF_003 core autofix: {}",
6248 out_al_disabled.sql
6249 );
6250 }
6251
6252 #[test]
6253 fn al001_fix_still_improves_with_fix_mode() {
6254 let sql = "SELECT * FROM a x JOIN b y ON x.id = y.id";
6255 assert_rule_case(sql, issue_codes::LINT_AL_001, 2, 0, 2);
6256
6257 let out = apply_lint_fixes(sql, Dialect::Generic, &[]).expect("fix result");
6258 let upper = out.sql.to_ascii_uppercase();
6259 assert!(
6260 upper.contains("FROM A AS X"),
6261 "expected explicit alias in fixed SQL, got: {}",
6262 out.sql
6263 );
6264 assert!(
6265 upper.contains("JOIN B AS Y"),
6266 "expected explicit alias in fixed SQL, got: {}",
6267 out.sql
6268 );
6269 }
6270
6271 #[test]
6272 fn al001_fix_does_not_synthesize_missing_aliases() {
6273 let sql = "SELECT COUNT(1) FROM users";
6274 let out = apply_lint_fixes(sql, Dialect::Generic, &[]).expect("fix result");
6275
6276 assert!(
6277 out.sql.to_ascii_uppercase().contains("COUNT(*)"),
6278 "expected non-AL001 fix to apply: {}",
6279 out.sql
6280 );
6281 assert!(
6282 !out.sql.to_ascii_uppercase().contains(" AS T"),
6283 "AL001 fixer must not generate synthetic aliases: {}",
6284 out.sql
6285 );
6286 }
6287
6288 #[test]
6289 fn al001_disabled_preserves_implicit_aliases_when_other_rules_fix() {
6290 let sql = "select count(1) from a x join b y on x.id = y.id";
6291 let out = apply_lint_fixes(
6292 sql,
6293 Dialect::Generic,
6294 &[issue_codes::LINT_AL_001.to_string()],
6295 )
6296 .expect("fix result");
6297
6298 assert!(
6299 out.sql.to_ascii_uppercase().contains("COUNT(*)"),
6300 "expected non-AL001 fix to apply: {}",
6301 out.sql
6302 );
6303 assert!(
6304 out.sql.to_ascii_uppercase().contains("FROM A X"),
6305 "implicit alias should be preserved when AL001 is disabled: {}",
6306 out.sql
6307 );
6308 assert!(
6309 out.sql.to_ascii_uppercase().contains("JOIN B Y"),
6310 "implicit alias should be preserved when AL001 is disabled: {}",
6311 out.sql
6312 );
6313 assert!(
6314 lint_rule_count(&out.sql, issue_codes::LINT_AL_001) > 0,
6315 "AL001 violations should remain when the rule is disabled: {}",
6316 out.sql
6317 );
6318 }
6319
6320 #[test]
6321 fn al001_implicit_config_rewrites_explicit_aliases() {
6322 let lint_config = LintConfig {
6323 enabled: true,
6324 disabled_rules: vec![],
6325 rule_configs: std::collections::BTreeMap::from([(
6326 issue_codes::LINT_AL_001.to_string(),
6327 serde_json::json!({"aliasing": "implicit"}),
6328 )]),
6329 };
6330
6331 let sql = "SELECT COUNT(1) FROM a AS x JOIN b AS y ON x.id = y.id";
6332 assert_eq!(
6333 lint_rule_count_with_config(sql, issue_codes::LINT_AL_001, &lint_config),
6334 2,
6335 "explicit aliases should violate AL001 under implicit mode"
6336 );
6337
6338 let out = apply_fix_with_config(sql, &lint_config);
6339 assert!(
6340 out.sql.to_ascii_uppercase().contains("COUNT(*)"),
6341 "expected non-AL001 fix to apply: {}",
6342 out.sql
6343 );
6344 assert!(
6345 !out.sql.to_ascii_uppercase().contains(" AS X"),
6346 "implicit-mode AL001 should remove explicit aliases: {}",
6347 out.sql
6348 );
6349 assert!(
6350 !out.sql.to_ascii_uppercase().contains(" AS Y"),
6351 "implicit-mode AL001 should remove explicit aliases: {}",
6352 out.sql
6353 );
6354 assert_eq!(
6355 lint_rule_count_with_config(&out.sql, issue_codes::LINT_AL_001, &lint_config),
6356 0,
6357 "AL001 should be resolved under implicit mode: {}",
6358 out.sql
6359 );
6360 }
6361
6362 #[test]
6363 fn table_alias_occurrences_handles_with_insert_select_aliases() {
6364 let sql = r#"
6365WITH params AS (
6366 SELECT now() - interval '1 day' AS period_start, now() AS period_end
6367),
6368overall AS (
6369 SELECT route, nav_type, mark FROM metrics.page_performance
6370),
6371device_breakdown AS (
6372 SELECT route, nav_type, mark FROM (
6373 SELECT route, nav_type, mark FROM metrics.page_performance
6374 ) sub
6375),
6376network_breakdown AS (
6377 SELECT route, nav_type, mark FROM (
6378 SELECT route, nav_type, mark FROM metrics.page_performance
6379 ) sub
6380),
6381version_breakdown AS (
6382 SELECT route, nav_type, mark FROM (
6383 SELECT route, nav_type, mark FROM metrics.page_performance
6384 ) sub
6385)
6386INSERT INTO metrics.page_performance_summary (route, period_start, period_end, nav_type, mark)
6387SELECT o.route, p.period_start, p.period_end, o.nav_type, o.mark
6388FROM overall o
6389CROSS JOIN params p
6390LEFT JOIN device_breakdown d ON d.route = o.route
6391LEFT JOIN network_breakdown n ON n.route = o.route
6392LEFT JOIN version_breakdown v ON v.route = o.route
6393ON CONFLICT (route, period_start, nav_type, mark) DO UPDATE SET
6394 period_end = EXCLUDED.period_end;
6395"#;
6396
6397 let occurrences = table_alias_occurrences(sql, Dialect::Postgres)
6398 .expect("alias occurrences should parse");
6399 let implicit_count = occurrences
6400 .iter()
6401 .filter(|alias| !alias.explicit_as)
6402 .count();
6403 assert!(
6404 implicit_count >= 8,
6405 "expected implicit aliases in CTE+INSERT query, got {}: {:?}",
6406 implicit_count,
6407 occurrences
6408 .iter()
6409 .map(|alias| (&alias.alias_key, alias.explicit_as))
6410 .collect::<Vec<_>>()
6411 );
6412 }
6413
6414 #[test]
6415 fn excluded_rule_is_not_rewritten_when_other_rules_are_fixed() {
6416 let sql = "SELECT COUNT(1) FROM t WHERE a<>b";
6417 let disabled = vec![issue_codes::LINT_CV_001.to_string()];
6418 let out = apply_lint_fixes(sql, Dialect::Generic, &disabled).expect("fix result");
6419 assert!(
6420 out.sql.to_ascii_uppercase().contains("COUNT(*)"),
6421 "expected COUNT style fix: {}",
6422 out.sql
6423 );
6424 assert!(
6425 out.sql.contains("<>"),
6426 "excluded CV_005 should remain '<>' (not '!='): {}",
6427 out.sql
6428 );
6429 assert!(
6430 !out.sql.contains("!="),
6431 "excluded CV_005 should not be rewritten to '!=': {}",
6432 out.sql
6433 );
6434 }
6435
6436 #[test]
6437 fn references_quoting_fix_keeps_reserved_identifier_quotes() {
6438 let sql = "SELECT \"FROM\" FROM t UNION SELECT 2";
6439 let out = apply_lint_fixes(sql, Dialect::Generic, &[]).expect("fix result");
6440 assert!(
6441 out.sql.contains("\"FROM\""),
6442 "reserved identifier must remain quoted: {}",
6443 out.sql
6444 );
6445 }
6446
6447 #[test]
6448 fn references_quoting_fix_unquotes_case_insensitive_dialect() {
6449 let sql = "SELECT \"CamelCase\" FROM t UNION SELECT 2";
6452 let out = apply_lint_fixes(
6453 sql,
6454 Dialect::Generic,
6455 &[issue_codes::LINT_LT_011.to_string()],
6456 )
6457 .expect("fix result");
6458 assert!(
6459 out.sql.contains("CamelCase") && !out.sql.contains("\"CamelCase\""),
6460 "case-insensitive dialect should unquote: {}",
6461 out.sql
6462 );
6463 assert!(
6464 out.sql.to_ascii_uppercase().contains("DISTINCT SELECT"),
6465 "expected another fix to persist output: {}",
6466 out.sql
6467 );
6468 }
6469
6470 #[test]
6471 fn references_quoting_fix_keeps_case_sensitive_identifier_quotes() {
6472 let sql = "SELECT \"CamelCase\" FROM t UNION SELECT 2";
6475 let out = apply_lint_fixes(
6476 sql,
6477 Dialect::Postgres,
6478 &[issue_codes::LINT_LT_011.to_string()],
6479 )
6480 .expect("fix result");
6481 assert!(
6482 out.sql.contains("\"CamelCase\""),
6483 "case-sensitive identifier must remain quoted: {}",
6484 out.sql
6485 );
6486 }
6487
6488 #[test]
6489 fn sqlfluff_fix_rule_smoke_cases_reduce_target_violations() {
6490 let cases = vec![
6491 (
6492 issue_codes::LINT_AL_001,
6493 "SELECT * FROM a x JOIN b y ON x.id = y.id",
6494 ),
6495 (
6496 issue_codes::LINT_AL_005,
6497 "SELECT u.name FROM users u JOIN orders o ON users.id = orders.user_id",
6498 ),
6499 (issue_codes::LINT_AL_009, "SELECT a AS a FROM t"),
6500 (issue_codes::LINT_AM_002, "SELECT 1 UNION SELECT 2"),
6501 (
6502 issue_codes::LINT_AM_003,
6503 "SELECT * FROM t ORDER BY a, b DESC",
6504 ),
6505 (
6506 issue_codes::LINT_AM_005,
6507 "SELECT * FROM a JOIN b ON a.id = b.id",
6508 ),
6509 (
6510 issue_codes::LINT_AM_008,
6511 "SELECT foo.a, bar.b FROM foo INNER JOIN bar",
6512 ),
6513 (issue_codes::LINT_CP_001, "SELECT a from t"),
6514 (issue_codes::LINT_CP_002, "SELECT Col, col FROM t"),
6515 (issue_codes::LINT_CP_003, "SELECT COUNT(*), count(name) FROM t"),
6516 (issue_codes::LINT_CP_004, "SELECT NULL, true FROM t"),
6517 (
6518 issue_codes::LINT_CP_005,
6519 "CREATE TABLE t (a INT, b varchar(10))",
6520 ),
6521 (
6522 issue_codes::LINT_CV_001,
6523 "SELECT * FROM t WHERE a <> b AND c != d",
6524 ),
6525 (
6526 issue_codes::LINT_CV_002,
6527 "SELECT IFNULL(x, 'default') FROM t",
6528 ),
6529 (issue_codes::LINT_CV_003, "SELECT a, FROM t"),
6530 (issue_codes::LINT_CV_004, "SELECT COUNT(1) FROM t"),
6531 (issue_codes::LINT_CV_005, "SELECT * FROM t WHERE a = NULL"),
6532 (issue_codes::LINT_CV_006, "SELECT 1 ;"),
6533 (issue_codes::LINT_CV_007, "(SELECT 1)"),
6534 (
6535 issue_codes::LINT_CV_012,
6536 "SELECT a.x, b.y FROM a JOIN b WHERE a.id = b.id",
6537 ),
6538 (issue_codes::LINT_JJ_001, "SELECT '{{foo}}' AS templated"),
6539 (issue_codes::LINT_LT_001, "SELECT payload->>'id' FROM t"),
6540 (issue_codes::LINT_LT_002, "SELECT a\n , b\nFROM t"),
6541 (issue_codes::LINT_LT_003, "SELECT a +\n b FROM t"),
6542 (issue_codes::LINT_LT_004, "SELECT a,b FROM t"),
6543 (issue_codes::LINT_LT_006, "SELECT COUNT (1) FROM t"),
6544 (
6545 issue_codes::LINT_LT_007,
6546 "WITH cte AS (\n SELECT 1) SELECT * FROM cte",
6547 ),
6548 (issue_codes::LINT_LT_009, "SELECT a,b,c,d,e FROM t"),
6549 (issue_codes::LINT_LT_010, "SELECT\nDISTINCT a\nFROM t"),
6550 (
6551 issue_codes::LINT_LT_011,
6552 "SELECT 1 UNION SELECT 2\nUNION SELECT 3",
6553 ),
6554 (issue_codes::LINT_LT_012, "SELECT 1\nFROM t"),
6555 (issue_codes::LINT_LT_013, "\n\nSELECT 1"),
6556 (issue_codes::LINT_LT_014, "SELECT a FROM t\nWHERE a=1"),
6557 (issue_codes::LINT_LT_015, "SELECT 1\n\n\nFROM t"),
6558 (issue_codes::LINT_RF_003, "SELECT a.id, id2 FROM a"),
6559 (issue_codes::LINT_RF_006, "SELECT \"good_name\" FROM t"),
6560 (
6561 issue_codes::LINT_ST_001,
6562 "SELECT CASE WHEN x > 1 THEN 'a' ELSE NULL END FROM t",
6563 ),
6564 (
6565 issue_codes::LINT_ST_004,
6566 "SELECT CASE WHEN species = 'Rat' THEN 'Squeak' ELSE CASE WHEN species = 'Dog' THEN 'Woof' END END FROM mytable",
6567 ),
6568 (
6569 issue_codes::LINT_ST_002,
6570 "SELECT CASE WHEN x > 0 THEN true ELSE false END FROM t",
6571 ),
6572 (
6573 issue_codes::LINT_ST_005,
6574 "SELECT * FROM t JOIN (SELECT * FROM u) sub ON t.id = sub.id",
6575 ),
6576 (issue_codes::LINT_ST_006, "SELECT a + 1, a FROM t"),
6577 (
6578 issue_codes::LINT_ST_007,
6579 "SELECT * FROM a JOIN b USING (id)",
6580 ),
6581 (issue_codes::LINT_ST_008, "SELECT DISTINCT(a) FROM t"),
6582 (
6583 issue_codes::LINT_ST_009,
6584 "SELECT * FROM a x JOIN b y ON y.id = x.id",
6585 ),
6586 (issue_codes::LINT_ST_012, "SELECT 1;;"),
6587 ];
6588
6589 for (code, sql) in cases {
6590 let before = lint_rule_count(sql, code);
6591 assert!(before > 0, "expected {code} to trigger before fix: {sql}");
6592 let out = apply_lint_fixes(sql, Dialect::Generic, &[]).expect("fix result");
6593 assert!(
6594 !out.skipped_due_to_comments,
6595 "test SQL should not be skipped: {sql}"
6596 );
6597 let after = lint_rule_count(&out.sql, code);
6598 assert!(
6599 after < before || out.sql != sql,
6600 "expected {code} count to decrease or SQL to be rewritten. before={before} after={after}\ninput={sql}\noutput={}",
6601 out.sql
6602 );
6603 }
6604 }
6605
6606 #[test]
6609 fn cv012_simple_where_join_to_on() {
6610 let sql = "SELECT a.x, b.y FROM a JOIN b WHERE a.id = b.id";
6611 let out = apply_lint_fixes(sql, Dialect::Generic, &[]).expect("fix");
6612 let lower = out.sql.to_ascii_lowercase();
6613 assert!(
6614 lower.contains(" on ") && !lower.contains("where"),
6615 "expected JOIN ON without WHERE: {}",
6616 out.sql
6617 );
6618 }
6619
6620 #[test]
6621 fn cv012_mixed_where_keeps_non_join_predicates() {
6622 let sql = "SELECT a.x FROM a JOIN b WHERE a.id = b.id AND a.val > 10";
6623 let out = apply_lint_fixes(sql, Dialect::Generic, &[]).expect("fix");
6624 let lower = out.sql.to_ascii_lowercase();
6625 assert!(lower.contains(" on "), "expected JOIN ON: {}", out.sql);
6626 assert!(
6627 lower.contains("where"),
6628 "expected remaining WHERE: {}",
6629 out.sql
6630 );
6631 }
6632
6633 #[test]
6634 fn cv012_multi_join_chain() {
6635 let sql = "SELECT * FROM a JOIN b JOIN c WHERE a.id = b.id AND b.id = c.id";
6636 let out = apply_lint_fixes(
6637 sql,
6638 Dialect::Generic,
6639 &[issue_codes::LINT_AM_005.to_string()],
6640 )
6641 .expect("fix");
6642 let lower = out.sql.to_ascii_lowercase();
6643 let on_count = lower.matches(" on ").count();
6645 assert!(on_count >= 2, "expected at least 2 ON clauses: {}", out.sql);
6646 assert!(
6647 !lower.contains("where"),
6648 "all predicates should be extracted: {}",
6649 out.sql
6650 );
6651 }
6652
6653 #[test]
6654 fn cv012_preserves_explicit_on() {
6655 let sql = "SELECT * FROM a JOIN b ON a.id = b.id";
6656 let out = apply_lint_fixes(sql, Dialect::Generic, &[]).expect("fix");
6657 assert_eq!(
6658 lint_rule_count(sql, issue_codes::LINT_CV_012),
6659 0,
6660 "explicit ON should not trigger CV_012"
6661 );
6662 let lower = out.sql.to_ascii_lowercase();
6663 assert!(
6664 lower.contains("on a.id = b.id"),
6665 "ON clause should be preserved: {}",
6666 out.sql
6667 );
6668 }
6669
6670 #[test]
6671 fn cv012_idempotent() {
6672 let sql = "SELECT a.x, b.y FROM a JOIN b WHERE a.id = b.id";
6673 let lint_config = LintConfig {
6674 enabled: true,
6675 disabled_rules: vec![issue_codes::LINT_LT_014.to_string()],
6676 rule_configs: std::collections::BTreeMap::new(),
6677 };
6678 let out1 = apply_lint_fixes_with_options(
6679 sql,
6680 Dialect::Generic,
6681 &lint_config,
6682 FixOptions {
6683 include_unsafe_fixes: true,
6684 include_rewrite_candidates: false,
6685 },
6686 )
6687 .expect("fix");
6688 let out2 = apply_lint_fixes_with_options(
6689 &out1.sql,
6690 Dialect::Generic,
6691 &lint_config,
6692 FixOptions {
6693 include_unsafe_fixes: true,
6694 include_rewrite_candidates: false,
6695 },
6696 )
6697 .expect("fix2");
6698 assert_eq!(
6699 out1.sql.trim_end(),
6700 out2.sql.trim_end(),
6701 "second pass should be idempotent aside from trailing-whitespace normalization"
6702 );
6703 }
6704}