1use crate::linter::config::LintConfig;
6use crate::linter::rule::{LintContext, LintRule};
7use crate::types::{issue_codes, Dialect, Issue, IssueAutofixApplicability, IssuePatchEdit, Span};
8use sqlparser::ast::{
9 Expr, FunctionArg, FunctionArgExpr, FunctionArguments, Select, SelectItem, Spanned, Statement,
10 TableFactor, WindowType,
11};
12use std::collections::HashSet;
13
14use super::semantic_helpers::{
15 select_projection_alias_set, select_source_count, visit_select_expressions,
16 visit_selects_in_statement,
17};
18
19#[derive(Clone, Copy, Debug, Eq, PartialEq)]
20enum SingleTableReferencesMode {
21 Consistent,
22 Qualified,
23 Unqualified,
24}
25
26impl SingleTableReferencesMode {
27 fn from_config(config: &LintConfig) -> Self {
28 match config
29 .rule_option_str(issue_codes::LINT_RF_003, "single_table_references")
30 .unwrap_or("consistent")
31 .to_ascii_lowercase()
32 .as_str()
33 {
34 "qualified" => Self::Qualified,
35 "unqualified" => Self::Unqualified,
36 _ => Self::Consistent,
37 }
38 }
39
40 fn violation(self, qualified: usize, unqualified: usize) -> bool {
41 match self {
42 Self::Consistent => qualified > 0 && unqualified > 0,
43 Self::Qualified => unqualified > 0,
44 Self::Unqualified => qualified > 0,
45 }
46 }
47}
48
49pub struct ReferencesConsistent {
50 single_table_references: SingleTableReferencesMode,
51 force_enable: bool,
52}
53
54#[derive(Clone)]
55struct Rf003SelectScope {
56 start: usize,
57 end: usize,
58 sources: HashSet<String>,
59}
60
61impl ReferencesConsistent {
62 pub fn from_config(config: &LintConfig) -> Self {
63 Self {
64 single_table_references: SingleTableReferencesMode::from_config(config),
65 force_enable: config
66 .rule_option_bool(issue_codes::LINT_RF_003, "force_enable")
67 .unwrap_or(true),
68 }
69 }
70}
71
72impl Default for ReferencesConsistent {
73 fn default() -> Self {
74 Self {
75 single_table_references: SingleTableReferencesMode::Consistent,
76 force_enable: true,
77 }
78 }
79}
80
81impl LintRule for ReferencesConsistent {
82 fn code(&self) -> &'static str {
83 issue_codes::LINT_RF_003
84 }
85
86 fn name(&self) -> &'static str {
87 "References consistent"
88 }
89
90 fn description(&self) -> &'static str {
91 "Column references should be qualified consistently in single table statements."
92 }
93
94 fn check(&self, statement: &Statement, ctx: &LintContext) -> Vec<Issue> {
95 if !self.force_enable {
96 return Vec::new();
97 }
98
99 let mut select_scopes = Vec::new();
100 visit_selects_in_statement(statement, &mut |select| {
101 if let Some((start, end)) = select_span_offsets(ctx.sql, select) {
102 select_scopes.push(Rf003SelectScope {
103 start,
104 end,
105 sources: select_source_names(select),
106 });
107 }
108 });
109
110 let mut mixed_count = 0usize;
111 let mut consistency_transition_count = 0usize;
112 let mut autofix_edits_raw: Vec<Rf003AutofixEdit> = Vec::new();
113
114 visit_selects_in_statement(statement, &mut |select| {
115 if select_source_count(select) != 1 {
116 return;
117 }
118 if select_contains_pivot(select) || select_contains_table_variable_source(select) {
119 return;
120 }
121
122 let aliases = select_projection_alias_set(select);
123 let source_names = select_source_names(select);
124 let ancestor_sources = ancestor_source_names_for_select(ctx, select, &select_scopes);
125 let (mut qualified, mut unqualified, has_outer_references) =
126 count_reference_qualification_for_select(
127 select,
128 &aliases,
129 &source_names,
130 &ancestor_sources,
131 ctx.dialect(),
132 );
133 let (projection_qualified, projection_unqualified) =
134 projection_wildcard_qualification_counts(select);
135 qualified += projection_qualified;
136 unqualified += projection_unqualified;
137
138 if has_outer_references
141 && self.single_table_references == SingleTableReferencesMode::Unqualified
142 {
143 return;
144 }
145
146 if self
147 .single_table_references
148 .violation(qualified, unqualified)
149 {
150 mixed_count += 1;
151 if self.single_table_references == SingleTableReferencesMode::Consistent
152 && qualified > 0
153 && unqualified > 0
154 {
155 consistency_transition_count += 1;
158 }
159
160 let target_style = match self.single_table_references {
161 SingleTableReferencesMode::Consistent
162 | SingleTableReferencesMode::Qualified => {
163 Some(Rf003AutofixTargetStyle::Qualify)
164 }
165 SingleTableReferencesMode::Unqualified => {
166 Some(Rf003AutofixTargetStyle::Unqualify)
167 }
168 };
169
170 if let Some(target_style) = target_style {
171 autofix_edits_raw.extend(rf003_autofix_edits_for_select(
172 select,
173 ctx,
174 target_style,
175 &aliases,
176 &source_names,
177 &ancestor_sources,
178 ));
179 }
180 }
181 });
182
183 if mixed_count == 0 {
184 return Vec::new();
185 }
186
187 if autofix_edits_raw.is_empty()
188 && self.single_table_references == SingleTableReferencesMode::Unqualified
189 {
190 let sql = ctx.statement_sql();
191 if let Some((table_name, alias)) = extract_from_table_and_alias(sql) {
192 let prefix = if alias.is_empty() {
193 table_name.rsplit('.').next().unwrap_or(&table_name)
194 } else {
195 alias.as_str()
196 };
197 if !prefix.is_empty() {
198 let rewritten = unqualify_prefix_in_sql_slice(sql, prefix);
199 if rewritten != sql {
200 autofix_edits_raw.push(Rf003AutofixEdit {
201 start: 0,
202 end: sql.len(),
203 replacement: rewritten,
204 });
205 }
206 }
207 }
208 }
209 if autofix_edits_raw.is_empty() {
210 autofix_edits_raw.extend(mixed_reference_autofix_edits(ctx.statement_sql()));
212 }
213 autofix_edits_raw.sort_by_key(|edit| (edit.start, edit.end));
214 autofix_edits_raw.dedup_by_key(|edit| (edit.start, edit.end));
215
216 let autofix_edits: Vec<IssuePatchEdit> = autofix_edits_raw
217 .into_iter()
218 .map(|edit| {
219 IssuePatchEdit::new(
220 ctx.span_from_statement_offset(edit.start, edit.end),
221 edit.replacement,
222 )
223 })
224 .collect();
225
226 if !autofix_edits.is_empty() || consistency_transition_count > 0 {
229 let mut issues: Vec<Issue> = autofix_edits
230 .into_iter()
231 .map(|edit| {
232 let span = Span::new(edit.span.start, edit.span.end);
233 Issue::info(
234 issue_codes::LINT_RF_003,
235 "Avoid mixing qualified and unqualified references.",
236 )
237 .with_statement(ctx.statement_index)
238 .with_span(span)
239 .with_autofix_edits(IssueAutofixApplicability::Safe, vec![edit])
240 })
241 .collect();
242 issues.extend((0..consistency_transition_count).map(|_| {
243 Issue::info(
244 issue_codes::LINT_RF_003,
245 "Avoid mixing qualified and unqualified references.",
246 )
247 .with_statement(ctx.statement_index)
248 }));
249 return issues;
250 }
251
252 (0..mixed_count)
254 .map(|_| {
255 Issue::info(
256 issue_codes::LINT_RF_003,
257 "Avoid mixing qualified and unqualified references.",
258 )
259 .with_statement(ctx.statement_index)
260 })
261 .collect()
262 }
263}
264
265fn ancestor_source_names_for_select(
266 ctx: &LintContext,
267 select: &Select,
268 scopes: &[Rf003SelectScope],
269) -> HashSet<String> {
270 let Some((start, end)) = select_span_offsets(ctx.sql, select) else {
271 return HashSet::new();
272 };
273
274 let mut out = HashSet::new();
275 for scope in scopes {
276 let strictly_contains =
277 scope.start <= start && scope.end >= end && (scope.start != start || scope.end != end);
278 if strictly_contains {
279 out.extend(scope.sources.iter().cloned());
280 }
281 }
282 out
283}
284
285struct Rf003AutofixEdit {
286 start: usize,
287 end: usize,
288 replacement: String,
289}
290
291#[derive(Clone, Copy, Debug, Eq, PartialEq)]
292enum Rf003AutofixTargetStyle {
293 Qualify,
294 Unqualify,
295}
296
297#[derive(Clone, Copy, Debug, Eq, PartialEq)]
298enum Rf003ReferenceClass {
299 Unqualified,
300 LocalQualified,
301 ObjectPath,
302 Ignore,
303}
304
305fn rf003_autofix_edits_for_select(
306 select: &Select,
307 ctx: &LintContext,
308 target_style: Rf003AutofixTargetStyle,
309 aliases: &HashSet<String>,
310 local_sources: &HashSet<String>,
311 statement_sources: &HashSet<String>,
312) -> Vec<Rf003AutofixEdit> {
313 let Some(prefix) = preferred_qualification_prefix(select) else {
314 return Vec::new();
315 };
316 if prefix.is_empty() {
317 return Vec::new();
318 }
319
320 let statement_sql = ctx.statement_sql();
321 let mut edits = Vec::new();
322 visit_select_expressions(select, &mut |expr| {
323 collect_rf003_autofix_edits_in_expr(
324 expr,
325 ctx,
326 statement_sql,
327 target_style,
328 &prefix,
329 aliases,
330 local_sources,
331 statement_sources,
332 ctx.dialect(),
333 &mut edits,
334 );
335 });
336
337 if edits.is_empty() && target_style == Rf003AutofixTargetStyle::Unqualify {
338 if let Some((start, end)) = select_statement_offsets(ctx, select) {
339 if start < end && end <= statement_sql.len() {
340 let original = &statement_sql[start..end];
341 let rewritten = unqualify_prefix_in_sql_slice(original, &prefix);
342 if rewritten != original {
343 edits.push(Rf003AutofixEdit {
344 start,
345 end,
346 replacement: rewritten,
347 });
348 }
349 }
350 }
351 }
352
353 edits
354}
355
356fn preferred_qualification_prefix(select: &Select) -> Option<String> {
357 let table = select.from.first()?;
358 match &table.relation {
359 TableFactor::Table { name, alias, .. } => {
360 if let Some(alias) = alias {
361 return Some(alias.name.value.clone());
362 }
363 let table_name = name.to_string();
364 let last = table_name.rsplit('.').next().unwrap_or(&table_name).trim();
365 (!last.is_empty()).then_some(last.to_string())
366 }
367 TableFactor::Derived { alias, .. }
368 | TableFactor::TableFunction { alias, .. }
369 | TableFactor::Function { alias, .. }
370 | TableFactor::UNNEST { alias, .. }
371 | TableFactor::JsonTable { alias, .. }
372 | TableFactor::OpenJsonTable { alias, .. }
373 | TableFactor::NestedJoin { alias, .. }
374 | TableFactor::Pivot { alias, .. }
375 | TableFactor::Unpivot { alias, .. }
376 | TableFactor::MatchRecognize { alias, .. } => alias.as_ref().map(|a| a.name.value.clone()),
377 _ => None,
378 }
379}
380
381#[allow(clippy::too_many_arguments)]
382fn collect_rf003_autofix_edits_in_expr(
383 expr: &Expr,
384 ctx: &LintContext,
385 statement_sql: &str,
386 target_style: Rf003AutofixTargetStyle,
387 prefix: &str,
388 aliases: &HashSet<String>,
389 local_sources: &HashSet<String>,
390 statement_sources: &HashSet<String>,
391 dialect: Dialect,
392 edits: &mut Vec<Rf003AutofixEdit>,
393) {
394 match expr {
395 Expr::Identifier(_) | Expr::CompoundIdentifier(_) => {
396 let class =
397 classify_rf003_reference(expr, aliases, local_sources, statement_sources, dialect);
398 let Some((start, end)) = expr_statement_offsets(ctx, expr) else {
399 return;
400 };
401 if start >= end || end > statement_sql.len() {
402 return;
403 }
404 let original = &statement_sql[start..end];
405
406 let replacement = match (target_style, class) {
407 (Rf003AutofixTargetStyle::Qualify, Rf003ReferenceClass::Unqualified)
408 | (Rf003AutofixTargetStyle::Qualify, Rf003ReferenceClass::ObjectPath) => {
409 Some(format!("{prefix}.{original}"))
410 }
411 (Rf003AutofixTargetStyle::Unqualify, Rf003ReferenceClass::LocalQualified) => {
412 original
413 .find('.')
414 .map(|dot| original[dot + 1..].to_string())
415 .filter(|rest| !rest.is_empty())
416 }
417 _ => None,
418 };
419
420 if let Some(replacement) = replacement {
421 if replacement != original {
422 edits.push(Rf003AutofixEdit {
423 start,
424 end,
425 replacement,
426 });
427 }
428 }
429 }
430 Expr::BinaryOp { left, right, .. }
431 | Expr::AnyOp { left, right, .. }
432 | Expr::AllOp { left, right, .. } => {
433 collect_rf003_autofix_edits_in_expr(
434 left,
435 ctx,
436 statement_sql,
437 target_style,
438 prefix,
439 aliases,
440 local_sources,
441 statement_sources,
442 dialect,
443 edits,
444 );
445 collect_rf003_autofix_edits_in_expr(
446 right,
447 ctx,
448 statement_sql,
449 target_style,
450 prefix,
451 aliases,
452 local_sources,
453 statement_sources,
454 dialect,
455 edits,
456 );
457 }
458 Expr::UnaryOp { expr: inner, .. }
459 | Expr::Nested(inner)
460 | Expr::IsNull(inner)
461 | Expr::IsNotNull(inner)
462 | Expr::Cast { expr: inner, .. } => collect_rf003_autofix_edits_in_expr(
463 inner,
464 ctx,
465 statement_sql,
466 target_style,
467 prefix,
468 aliases,
469 local_sources,
470 statement_sources,
471 dialect,
472 edits,
473 ),
474 Expr::InList { expr, list, .. } => {
475 collect_rf003_autofix_edits_in_expr(
476 expr,
477 ctx,
478 statement_sql,
479 target_style,
480 prefix,
481 aliases,
482 local_sources,
483 statement_sources,
484 dialect,
485 edits,
486 );
487 for item in list {
488 collect_rf003_autofix_edits_in_expr(
489 item,
490 ctx,
491 statement_sql,
492 target_style,
493 prefix,
494 aliases,
495 local_sources,
496 statement_sources,
497 dialect,
498 edits,
499 );
500 }
501 }
502 Expr::Between {
503 expr, low, high, ..
504 } => {
505 collect_rf003_autofix_edits_in_expr(
506 expr,
507 ctx,
508 statement_sql,
509 target_style,
510 prefix,
511 aliases,
512 local_sources,
513 statement_sources,
514 dialect,
515 edits,
516 );
517 collect_rf003_autofix_edits_in_expr(
518 low,
519 ctx,
520 statement_sql,
521 target_style,
522 prefix,
523 aliases,
524 local_sources,
525 statement_sources,
526 dialect,
527 edits,
528 );
529 collect_rf003_autofix_edits_in_expr(
530 high,
531 ctx,
532 statement_sql,
533 target_style,
534 prefix,
535 aliases,
536 local_sources,
537 statement_sources,
538 dialect,
539 edits,
540 );
541 }
542 Expr::Case {
543 operand,
544 conditions,
545 else_result,
546 ..
547 } => {
548 if let Some(operand) = operand {
549 collect_rf003_autofix_edits_in_expr(
550 operand,
551 ctx,
552 statement_sql,
553 target_style,
554 prefix,
555 aliases,
556 local_sources,
557 statement_sources,
558 dialect,
559 edits,
560 );
561 }
562 for when in conditions {
563 collect_rf003_autofix_edits_in_expr(
564 &when.condition,
565 ctx,
566 statement_sql,
567 target_style,
568 prefix,
569 aliases,
570 local_sources,
571 statement_sources,
572 dialect,
573 edits,
574 );
575 collect_rf003_autofix_edits_in_expr(
576 &when.result,
577 ctx,
578 statement_sql,
579 target_style,
580 prefix,
581 aliases,
582 local_sources,
583 statement_sources,
584 dialect,
585 edits,
586 );
587 }
588 if let Some(otherwise) = else_result {
589 collect_rf003_autofix_edits_in_expr(
590 otherwise,
591 ctx,
592 statement_sql,
593 target_style,
594 prefix,
595 aliases,
596 local_sources,
597 statement_sources,
598 dialect,
599 edits,
600 );
601 }
602 }
603 Expr::Function(function) => {
604 if let FunctionArguments::List(arguments) = &function.args {
605 for (index, arg) in arguments.args.iter().enumerate() {
606 match arg {
607 FunctionArg::Unnamed(FunctionArgExpr::Expr(expr))
608 | FunctionArg::Named {
609 arg: FunctionArgExpr::Expr(expr),
610 ..
611 } => {
612 if should_skip_identifier_reference_for_function_arg(
613 function, index, expr,
614 ) {
615 continue;
616 }
617 collect_rf003_autofix_edits_in_expr(
618 expr,
619 ctx,
620 statement_sql,
621 target_style,
622 prefix,
623 aliases,
624 local_sources,
625 statement_sources,
626 dialect,
627 edits,
628 );
629 }
630 _ => {}
631 }
632 }
633 }
634 if let Some(filter) = &function.filter {
635 collect_rf003_autofix_edits_in_expr(
636 filter,
637 ctx,
638 statement_sql,
639 target_style,
640 prefix,
641 aliases,
642 local_sources,
643 statement_sources,
644 dialect,
645 edits,
646 );
647 }
648 for order_expr in &function.within_group {
649 collect_rf003_autofix_edits_in_expr(
650 &order_expr.expr,
651 ctx,
652 statement_sql,
653 target_style,
654 prefix,
655 aliases,
656 local_sources,
657 statement_sources,
658 dialect,
659 edits,
660 );
661 }
662 if let Some(WindowType::WindowSpec(spec)) = &function.over {
663 for expr in &spec.partition_by {
664 collect_rf003_autofix_edits_in_expr(
665 expr,
666 ctx,
667 statement_sql,
668 target_style,
669 prefix,
670 aliases,
671 local_sources,
672 statement_sources,
673 dialect,
674 edits,
675 );
676 }
677 for order_expr in &spec.order_by {
678 collect_rf003_autofix_edits_in_expr(
679 &order_expr.expr,
680 ctx,
681 statement_sql,
682 target_style,
683 prefix,
684 aliases,
685 local_sources,
686 statement_sources,
687 dialect,
688 edits,
689 );
690 }
691 }
692 }
693 Expr::InSubquery { expr, .. } => collect_rf003_autofix_edits_in_expr(
694 expr,
695 ctx,
696 statement_sql,
697 target_style,
698 prefix,
699 aliases,
700 local_sources,
701 statement_sources,
702 dialect,
703 edits,
704 ),
705 Expr::Exists { .. } | Expr::Subquery(_) => {}
706 _ => {}
707 }
708}
709
710fn classify_rf003_reference(
711 expr: &Expr,
712 aliases: &HashSet<String>,
713 local_sources: &HashSet<String>,
714 statement_sources: &HashSet<String>,
715 dialect: Dialect,
716) -> Rf003ReferenceClass {
717 match expr {
718 Expr::Identifier(identifier) => {
719 let name = identifier.value.to_ascii_uppercase();
720 if aliases.contains(&name) || identifier.value.starts_with('@') {
721 Rf003ReferenceClass::Ignore
722 } else {
723 Rf003ReferenceClass::Unqualified
724 }
725 }
726 Expr::CompoundIdentifier(parts) => {
727 if parts.is_empty() {
728 return Rf003ReferenceClass::Ignore;
729 }
730 let first = parts[0].value.to_ascii_uppercase();
731 if first.starts_with('@') {
732 return Rf003ReferenceClass::Ignore;
733 }
734 if parts.len() == 1 {
735 if aliases.contains(&first) {
736 Rf003ReferenceClass::Ignore
737 } else {
738 Rf003ReferenceClass::Unqualified
739 }
740 } else if local_sources.contains(&first) {
741 Rf003ReferenceClass::LocalQualified
742 } else if statement_sources.contains(&first) {
743 Rf003ReferenceClass::Ignore
744 } else if is_object_reference_dialect(dialect) {
745 Rf003ReferenceClass::ObjectPath
746 } else {
747 Rf003ReferenceClass::LocalQualified
748 }
749 }
750 _ => Rf003ReferenceClass::Ignore,
751 }
752}
753
754fn expr_statement_offsets(ctx: &LintContext, expr: &Expr) -> Option<(usize, usize)> {
755 if ctx.statement_range.start > 0 {
759 if let Some((start, end)) = expr_span_offsets(ctx.sql, expr) {
760 if start >= ctx.statement_range.start && end <= ctx.statement_range.end {
761 return Some((
762 start - ctx.statement_range.start,
763 end - ctx.statement_range.start,
764 ));
765 }
766 }
767 }
768
769 if let Some((start, end)) = expr_span_offsets(ctx.statement_sql(), expr) {
770 return Some((start, end));
771 }
772
773 let (start, end) = expr_span_offsets(ctx.sql, expr)?;
774 if start < ctx.statement_range.start || end > ctx.statement_range.end {
775 return None;
776 }
777
778 Some((
779 start - ctx.statement_range.start,
780 end - ctx.statement_range.start,
781 ))
782}
783
784fn select_statement_offsets(ctx: &LintContext, select: &Select) -> Option<(usize, usize)> {
785 if ctx.statement_range.start > 0 {
789 if let Some((start, end)) = select_span_offsets(ctx.sql, select) {
790 if start >= ctx.statement_range.start && end <= ctx.statement_range.end {
791 return Some((
792 start - ctx.statement_range.start,
793 end - ctx.statement_range.start,
794 ));
795 }
796 }
797 }
798
799 if let Some((start, end)) = select_span_offsets(ctx.statement_sql(), select) {
800 return Some((start, end));
801 }
802
803 let (start, end) = select_span_offsets(ctx.sql, select)?;
804 if start < ctx.statement_range.start || end > ctx.statement_range.end {
805 return None;
806 }
807
808 Some((
809 start - ctx.statement_range.start,
810 end - ctx.statement_range.start,
811 ))
812}
813
814fn expr_span_offsets(sql: &str, expr: &Expr) -> Option<(usize, usize)> {
815 let span = expr.span();
816 if span.start.line == 0 || span.start.column == 0 || span.end.line == 0 || span.end.column == 0
817 {
818 return None;
819 }
820
821 let start = line_col_to_offset(sql, span.start.line as usize, span.start.column as usize)?;
822 let end = line_col_to_offset(sql, span.end.line as usize, span.end.column as usize)?;
823 (end >= start).then_some((start, end))
824}
825
826fn select_span_offsets(sql: &str, select: &Select) -> Option<(usize, usize)> {
827 let span = select.span();
828 if span.start.line == 0 || span.start.column == 0 || span.end.line == 0 || span.end.column == 0
829 {
830 return None;
831 }
832
833 let start = line_col_to_offset(sql, span.start.line as usize, span.start.column as usize)?;
834 let end = line_col_to_offset(sql, span.end.line as usize, span.end.column as usize)?;
835 (end >= start).then_some((start, end))
836}
837
838fn line_col_to_offset(sql: &str, line: usize, column: usize) -> Option<usize> {
839 if line == 0 || column == 0 {
840 return None;
841 }
842
843 let mut current_line = 1usize;
844 let mut line_start = 0usize;
845
846 for (idx, ch) in sql.char_indices() {
847 if current_line == line {
848 break;
849 }
850 if ch == '\n' {
851 current_line += 1;
852 line_start = idx + ch.len_utf8();
853 }
854 }
855 if current_line != line {
856 return None;
857 }
858
859 let mut current_column = 1usize;
860 for (rel_idx, ch) in sql[line_start..].char_indices() {
861 if current_column == column {
862 return Some(line_start + rel_idx);
863 }
864 if ch == '\n' {
865 return None;
866 }
867 current_column += 1;
868 }
869
870 if current_column == column {
871 return Some(sql.len());
872 }
873
874 None
875}
876
877fn unqualify_prefix_in_sql_slice(sql: &str, prefix: &str) -> String {
878 let bytes = sql.as_bytes();
879 let prefix_bytes = prefix.as_bytes();
880 let mut out = String::with_capacity(sql.len());
881 let mut i = 0usize;
882
883 #[derive(Clone, Copy, PartialEq, Eq)]
884 enum Mode {
885 Outside,
886 SingleQuote,
887 DoubleQuote,
888 BacktickQuote,
889 BracketQuote,
890 LineComment,
891 BlockComment,
892 }
893
894 let mut mode = Mode::Outside;
895
896 while i < bytes.len() {
897 let b = bytes[i];
898 let next = bytes.get(i + 1).copied();
899
900 match mode {
901 Mode::Outside => {
902 if b == b'-' && next == Some(b'-') {
903 out.push('-');
904 out.push('-');
905 i += 2;
906 mode = Mode::LineComment;
907 continue;
908 }
909 if b == b'/' && next == Some(b'*') {
910 out.push('/');
911 out.push('*');
912 i += 2;
913 mode = Mode::BlockComment;
914 continue;
915 }
916 if b == b'\'' {
917 out.push('\'');
918 i += 1;
919 mode = Mode::SingleQuote;
920 continue;
921 }
922 if b == b'"' {
923 out.push('"');
924 i += 1;
925 mode = Mode::DoubleQuote;
926 continue;
927 }
928 if b == b'`' {
929 out.push('`');
930 i += 1;
931 mode = Mode::BacktickQuote;
932 continue;
933 }
934 if b == b'[' {
935 out.push('[');
936 i += 1;
937 mode = Mode::BracketQuote;
938 continue;
939 }
940
941 if i + prefix_bytes.len() + 1 < bytes.len()
942 && bytes[i..i + prefix_bytes.len()]
943 .iter()
944 .zip(prefix_bytes.iter())
945 .all(|(actual, expected)| actual.eq_ignore_ascii_case(expected))
946 && (i == 0
947 || !bytes[i - 1].is_ascii_alphanumeric()
948 && bytes[i - 1] != b'_'
949 && bytes[i - 1] != b'$')
950 && bytes[i + prefix_bytes.len()] == b'.'
951 {
952 i += prefix_bytes.len() + 1;
953 continue;
954 }
955
956 out.push(char::from(b));
957 i += 1;
958 }
959 Mode::SingleQuote => {
960 out.push(char::from(b));
961 i += 1;
962 if b == b'\'' {
963 if next == Some(b'\'') {
964 out.push('\'');
965 i += 1;
966 } else {
967 mode = Mode::Outside;
968 }
969 }
970 }
971 Mode::DoubleQuote => {
972 out.push(char::from(b));
973 i += 1;
974 if b == b'"' {
975 if next == Some(b'"') {
976 out.push('"');
977 i += 1;
978 } else {
979 mode = Mode::Outside;
980 }
981 }
982 }
983 Mode::BacktickQuote => {
984 out.push(char::from(b));
985 i += 1;
986 if b == b'`' {
987 if next == Some(b'`') {
988 out.push('`');
989 i += 1;
990 } else {
991 mode = Mode::Outside;
992 }
993 }
994 }
995 Mode::BracketQuote => {
996 out.push(char::from(b));
997 i += 1;
998 if b == b']' {
999 if next == Some(b']') {
1000 out.push(']');
1001 i += 1;
1002 } else {
1003 mode = Mode::Outside;
1004 }
1005 }
1006 }
1007 Mode::LineComment => {
1008 out.push(char::from(b));
1009 i += 1;
1010 if b == b'\n' {
1011 mode = Mode::Outside;
1012 }
1013 }
1014 Mode::BlockComment => {
1015 out.push(char::from(b));
1016 i += 1;
1017 if b == b'*' && next == Some(b'/') {
1018 out.push('/');
1019 i += 1;
1020 mode = Mode::Outside;
1021 }
1022 }
1023 }
1024 }
1025
1026 out
1027}
1028
1029fn mixed_reference_autofix_edits(sql: &str) -> Vec<Rf003AutofixEdit> {
1030 let bytes = sql.as_bytes();
1031 let Some(select_start) = find_ascii_keyword(bytes, b"SELECT", 0) else {
1032 return Vec::new();
1033 };
1034 let select_end = select_start + b"SELECT".len();
1035 let Some(from_start) = find_ascii_keyword(bytes, b"FROM", select_end) else {
1036 return Vec::new();
1037 };
1038
1039 let Some((table_name, alias)) = extract_from_table_and_alias(sql) else {
1040 return Vec::new();
1041 };
1042 let prefix = if alias.is_empty() {
1043 table_name.rsplit('.').next().unwrap_or(&table_name)
1044 } else {
1045 alias.as_str()
1046 };
1047 if prefix.is_empty() {
1048 return Vec::new();
1049 }
1050
1051 let select_clause = &sql[select_end..from_start];
1052 let projection_items = split_projection_items(select_clause);
1053 if projection_items.is_empty() {
1054 return Vec::new();
1055 }
1056
1057 let has_qualified = projection_items
1058 .iter()
1059 .any(|(value, _, _)| is_simple_qualified_identifier(value));
1060 let has_unqualified = projection_items
1061 .iter()
1062 .any(|(value, _, _)| is_simple_identifier(value));
1063 if !(has_qualified && has_unqualified) {
1064 return Vec::new();
1065 }
1066
1067 projection_items
1068 .into_iter()
1069 .filter_map(|(value, start, end)| {
1070 if !is_simple_identifier(&value) {
1071 return None;
1072 }
1073 Some(Rf003AutofixEdit {
1074 start: select_end + start,
1075 end: select_end + end,
1076 replacement: format!("{prefix}.{value}"),
1077 })
1078 })
1079 .collect()
1080}
1081
1082fn split_projection_items(select_clause: &str) -> Vec<(String, usize, usize)> {
1083 let bytes = select_clause.as_bytes();
1084 let mut out = Vec::new();
1085 let mut segment_start = 0usize;
1086 let mut index = 0usize;
1087
1088 while index <= bytes.len() {
1089 if index == bytes.len() || bytes[index] == b',' {
1090 let segment = &select_clause[segment_start..index];
1091 let leading_trim = segment
1092 .char_indices()
1093 .find(|(_, ch)| !ch.is_ascii_whitespace())
1094 .map(|(idx, _)| idx)
1095 .unwrap_or(segment.len());
1096 let trailing_trim = segment
1097 .char_indices()
1098 .rfind(|(_, ch)| !ch.is_ascii_whitespace())
1099 .map(|(idx, ch)| idx + ch.len_utf8())
1100 .unwrap_or(leading_trim);
1101
1102 if leading_trim < trailing_trim {
1103 let value = segment[leading_trim..trailing_trim].to_string();
1104 out.push((
1105 value,
1106 segment_start + leading_trim,
1107 segment_start + trailing_trim,
1108 ));
1109 }
1110 segment_start = index + 1;
1111 }
1112 index += 1;
1113 }
1114
1115 out
1116}
1117
1118fn extract_from_table_and_alias(sql: &str) -> Option<(String, String)> {
1119 let bytes = sql.as_bytes();
1120 let from_start = find_ascii_keyword(bytes, b"FROM", 0)?;
1121 let mut index = skip_ascii_whitespace(bytes, from_start + b"FROM".len());
1122 let table_start = index;
1123 index = consume_ascii_identifier(bytes, index)?;
1124 while index < bytes.len() && bytes[index] == b'.' {
1125 let next = consume_ascii_identifier(bytes, index + 1)?;
1126 index = next;
1127 }
1128 let table_name = sql[table_start..index].to_string();
1129
1130 let mut alias = String::new();
1131 let after_table = skip_ascii_whitespace(bytes, index);
1132 if after_table > index {
1133 if let Some(as_end) = match_ascii_keyword_at(bytes, after_table, b"AS") {
1134 let alias_start = skip_ascii_whitespace(bytes, as_end);
1135 if alias_start > as_end {
1136 if let Some(alias_end) = consume_ascii_identifier(bytes, alias_start) {
1137 alias = sql[alias_start..alias_end].to_string();
1138 }
1139 }
1140 } else if let Some(alias_end) = consume_ascii_identifier(bytes, after_table) {
1141 alias = sql[after_table..alias_end].to_string();
1142 }
1143 }
1144
1145 Some((table_name, alias))
1146}
1147
1148fn is_ascii_whitespace_byte(byte: u8) -> bool {
1149 matches!(byte, b' ' | b'\n' | b'\r' | b'\t' | 0x0b | 0x0c)
1150}
1151
1152fn is_ascii_ident_start(byte: u8) -> bool {
1153 byte.is_ascii_alphabetic() || byte == b'_'
1154}
1155
1156fn is_ascii_ident_continue(byte: u8) -> bool {
1157 byte.is_ascii_alphanumeric() || byte == b'_'
1158}
1159
1160fn skip_ascii_whitespace(bytes: &[u8], mut index: usize) -> usize {
1161 while index < bytes.len() && is_ascii_whitespace_byte(bytes[index]) {
1162 index += 1;
1163 }
1164 index
1165}
1166
1167fn consume_ascii_identifier(bytes: &[u8], start: usize) -> Option<usize> {
1168 if start >= bytes.len() || !is_ascii_ident_start(bytes[start]) {
1169 return None;
1170 }
1171 let mut index = start + 1;
1172 while index < bytes.len() && is_ascii_ident_continue(bytes[index]) {
1173 index += 1;
1174 }
1175 Some(index)
1176}
1177
1178fn is_word_boundary_for_keyword(bytes: &[u8], index: usize) -> bool {
1179 index == 0 || index >= bytes.len() || !is_ascii_ident_continue(bytes[index])
1180}
1181
1182fn match_ascii_keyword_at(bytes: &[u8], start: usize, keyword_upper: &[u8]) -> Option<usize> {
1183 let end = start.checked_add(keyword_upper.len())?;
1184 if end > bytes.len() {
1185 return None;
1186 }
1187 if !is_word_boundary_for_keyword(bytes, start.saturating_sub(1))
1188 || !is_word_boundary_for_keyword(bytes, end)
1189 {
1190 return None;
1191 }
1192 let matches = bytes[start..end]
1193 .iter()
1194 .zip(keyword_upper.iter())
1195 .all(|(actual, expected)| actual.to_ascii_uppercase() == *expected);
1196 if matches {
1197 Some(end)
1198 } else {
1199 None
1200 }
1201}
1202
1203fn find_ascii_keyword(bytes: &[u8], keyword_upper: &[u8], from: usize) -> Option<usize> {
1204 let mut index = from;
1205 while index + keyword_upper.len() <= bytes.len() {
1206 if match_ascii_keyword_at(bytes, index, keyword_upper).is_some() {
1207 return Some(index);
1208 }
1209 index += 1;
1210 }
1211 None
1212}
1213
1214fn is_simple_identifier(value: &str) -> bool {
1215 let bytes = value.as_bytes();
1216 if bytes.is_empty() || !is_ascii_ident_start(bytes[0]) {
1217 return false;
1218 }
1219 bytes[1..].iter().copied().all(is_ascii_ident_continue)
1220}
1221
1222fn is_simple_qualified_identifier(value: &str) -> bool {
1223 let mut parts = value.split('.');
1224 match (parts.next(), parts.next(), parts.next()) {
1225 (Some(left), Some(right), None) => {
1226 is_simple_identifier(left) && is_simple_identifier(right)
1227 }
1228 _ => false,
1229 }
1230}
1231
1232fn projection_wildcard_qualification_counts(select: &Select) -> (usize, usize) {
1233 let mut qualified = 0usize;
1234
1235 for item in &select.projection {
1236 match item {
1237 SelectItem::QualifiedWildcard(_, _) => qualified += 1,
1239 SelectItem::Wildcard(_) => {}
1241 _ => {}
1242 }
1243 }
1244
1245 (qualified, 0)
1246}
1247
1248fn select_source_names(select: &Select) -> HashSet<String> {
1249 let mut names = HashSet::new();
1250 for table in &select.from {
1251 collect_source_names_from_table_factor(&table.relation, &mut names);
1252 for join in &table.joins {
1253 collect_source_names_from_table_factor(&join.relation, &mut names);
1254 }
1255 }
1256 names
1257}
1258
1259fn collect_source_names_from_table_factor(table_factor: &TableFactor, names: &mut HashSet<String>) {
1260 match table_factor {
1261 TableFactor::Table { name, alias, .. } => {
1262 if let Some(alias) = alias {
1263 names.insert(alias.name.value.to_ascii_uppercase());
1264 }
1265 let table_name = name.to_string();
1266 if !table_name.is_empty() {
1267 let last = table_name
1268 .rsplit('.')
1269 .next()
1270 .unwrap_or(&table_name)
1271 .trim_matches(|ch| matches!(ch, '"' | '`' | '[' | ']'))
1272 .to_ascii_uppercase();
1273 if !last.is_empty() {
1274 names.insert(last);
1275 }
1276 }
1277 }
1278 TableFactor::Derived {
1279 alias: Some(alias), ..
1280 } => {
1281 names.insert(alias.name.value.to_ascii_uppercase());
1282 }
1283 TableFactor::Derived { alias: None, .. } => {}
1284 TableFactor::TableFunction { alias, .. }
1285 | TableFactor::Function { alias, .. }
1286 | TableFactor::UNNEST { alias, .. }
1287 | TableFactor::JsonTable { alias, .. }
1288 | TableFactor::OpenJsonTable { alias, .. } => {
1289 if let Some(alias) = alias {
1290 names.insert(alias.name.value.to_ascii_uppercase());
1291 }
1292 }
1293 TableFactor::NestedJoin {
1294 table_with_joins, ..
1295 } => {
1296 collect_source_names_from_table_factor(&table_with_joins.relation, names);
1297 for join in &table_with_joins.joins {
1298 collect_source_names_from_table_factor(&join.relation, names);
1299 }
1300 }
1301 TableFactor::Pivot { table, .. }
1302 | TableFactor::Unpivot { table, .. }
1303 | TableFactor::MatchRecognize { table, .. } => {
1304 collect_source_names_from_table_factor(table, names);
1305 }
1306 _ => {}
1307 }
1308}
1309
1310fn select_contains_pivot(select: &Select) -> bool {
1311 select.from.iter().any(|table| {
1312 table_factor_contains_pivot(&table.relation)
1313 || table
1314 .joins
1315 .iter()
1316 .any(|join| table_factor_contains_pivot(&join.relation))
1317 })
1318}
1319
1320fn table_factor_contains_pivot(table_factor: &TableFactor) -> bool {
1321 match table_factor {
1322 TableFactor::Pivot { .. } => true,
1323 TableFactor::NestedJoin {
1324 table_with_joins, ..
1325 } => {
1326 table_factor_contains_pivot(&table_with_joins.relation)
1327 || table_with_joins
1328 .joins
1329 .iter()
1330 .any(|join| table_factor_contains_pivot(&join.relation))
1331 }
1332 TableFactor::Unpivot { table, .. } | TableFactor::MatchRecognize { table, .. } => {
1333 table_factor_contains_pivot(table)
1334 }
1335 _ => false,
1336 }
1337}
1338
1339fn select_contains_table_variable_source(select: &Select) -> bool {
1340 select.from.iter().any(|table| {
1341 table_factor_contains_table_variable(&table.relation)
1342 || table
1343 .joins
1344 .iter()
1345 .any(|join| table_factor_contains_table_variable(&join.relation))
1346 })
1347}
1348
1349fn table_factor_contains_table_variable(table_factor: &TableFactor) -> bool {
1350 match table_factor {
1351 TableFactor::Table { name, .. } => name.to_string().trim_start().starts_with('@'),
1352 TableFactor::NestedJoin {
1353 table_with_joins, ..
1354 } => {
1355 table_factor_contains_table_variable(&table_with_joins.relation)
1356 || table_with_joins
1357 .joins
1358 .iter()
1359 .any(|join| table_factor_contains_table_variable(&join.relation))
1360 }
1361 TableFactor::Pivot { table, .. }
1362 | TableFactor::Unpivot { table, .. }
1363 | TableFactor::MatchRecognize { table, .. } => table_factor_contains_table_variable(table),
1364 _ => false,
1365 }
1366}
1367
1368fn count_reference_qualification_for_select(
1369 select: &Select,
1370 aliases: &HashSet<String>,
1371 local_sources: &HashSet<String>,
1372 statement_sources: &HashSet<String>,
1373 dialect: Dialect,
1374) -> (usize, usize, bool) {
1375 let mut qualified = 0usize;
1376 let mut unqualified = 0usize;
1377 let mut has_outer_references = false;
1378
1379 visit_select_expressions(select, &mut |expr| {
1380 let (q, u) = count_reference_qualification_in_expr_rf03(
1381 expr,
1382 aliases,
1383 local_sources,
1384 statement_sources,
1385 dialect,
1386 &mut has_outer_references,
1387 );
1388 qualified += q;
1389 unqualified += u;
1390 });
1391
1392 (qualified, unqualified, has_outer_references)
1393}
1394
1395fn count_reference_qualification_in_expr_rf03(
1396 expr: &Expr,
1397 aliases: &HashSet<String>,
1398 local_sources: &HashSet<String>,
1399 statement_sources: &HashSet<String>,
1400 dialect: Dialect,
1401 has_outer_references: &mut bool,
1402) -> (usize, usize) {
1403 match expr {
1404 Expr::Identifier(identifier) => {
1405 let name = identifier.value.to_ascii_uppercase();
1406 if aliases.contains(&name) || identifier.value.starts_with('@') {
1407 (0, 0)
1408 } else {
1409 (0, 1)
1410 }
1411 }
1412 Expr::CompoundIdentifier(parts) => {
1413 if parts.is_empty() {
1414 return (0, 0);
1415 }
1416
1417 let first = parts[0].value.to_ascii_uppercase();
1418 if first.starts_with('@') {
1419 return (0, 0);
1420 }
1421
1422 if parts.len() == 1 {
1423 if aliases.contains(&first) {
1424 return (0, 0);
1425 }
1426 return (0, 1);
1427 }
1428
1429 if local_sources.contains(&first) {
1430 (1, 0)
1431 } else if statement_sources.contains(&first) {
1432 *has_outer_references = true;
1433 (0, 0)
1434 } else if is_object_reference_dialect(dialect) {
1435 (0, 1)
1438 } else {
1439 (1, 0)
1440 }
1441 }
1442 Expr::BinaryOp { left, right, .. }
1443 | Expr::AnyOp { left, right, .. }
1444 | Expr::AllOp { left, right, .. } => {
1445 let (lq, lu) = count_reference_qualification_in_expr_rf03(
1446 left,
1447 aliases,
1448 local_sources,
1449 statement_sources,
1450 dialect,
1451 has_outer_references,
1452 );
1453 let (rq, ru) = count_reference_qualification_in_expr_rf03(
1454 right,
1455 aliases,
1456 local_sources,
1457 statement_sources,
1458 dialect,
1459 has_outer_references,
1460 );
1461 (lq + rq, lu + ru)
1462 }
1463 Expr::UnaryOp { expr: inner, .. }
1464 | Expr::Nested(inner)
1465 | Expr::IsNull(inner)
1466 | Expr::IsNotNull(inner)
1467 | Expr::Cast { expr: inner, .. } => count_reference_qualification_in_expr_rf03(
1468 inner,
1469 aliases,
1470 local_sources,
1471 statement_sources,
1472 dialect,
1473 has_outer_references,
1474 ),
1475 Expr::InList { expr, list, .. } => {
1476 let (mut q, mut u) = count_reference_qualification_in_expr_rf03(
1477 expr,
1478 aliases,
1479 local_sources,
1480 statement_sources,
1481 dialect,
1482 has_outer_references,
1483 );
1484 for item in list {
1485 let (iq, iu) = count_reference_qualification_in_expr_rf03(
1486 item,
1487 aliases,
1488 local_sources,
1489 statement_sources,
1490 dialect,
1491 has_outer_references,
1492 );
1493 q += iq;
1494 u += iu;
1495 }
1496 (q, u)
1497 }
1498 Expr::Between {
1499 expr, low, high, ..
1500 } => {
1501 let (eq, eu) = count_reference_qualification_in_expr_rf03(
1502 expr,
1503 aliases,
1504 local_sources,
1505 statement_sources,
1506 dialect,
1507 has_outer_references,
1508 );
1509 let (lq, lu) = count_reference_qualification_in_expr_rf03(
1510 low,
1511 aliases,
1512 local_sources,
1513 statement_sources,
1514 dialect,
1515 has_outer_references,
1516 );
1517 let (hq, hu) = count_reference_qualification_in_expr_rf03(
1518 high,
1519 aliases,
1520 local_sources,
1521 statement_sources,
1522 dialect,
1523 has_outer_references,
1524 );
1525 (eq + lq + hq, eu + lu + hu)
1526 }
1527 Expr::Case {
1528 operand,
1529 conditions,
1530 else_result,
1531 ..
1532 } => {
1533 let mut q = 0usize;
1534 let mut u = 0usize;
1535 if let Some(operand) = operand {
1536 let (oq, ou) = count_reference_qualification_in_expr_rf03(
1537 operand,
1538 aliases,
1539 local_sources,
1540 statement_sources,
1541 dialect,
1542 has_outer_references,
1543 );
1544 q += oq;
1545 u += ou;
1546 }
1547 for when in conditions {
1548 let (cq, cu) = count_reference_qualification_in_expr_rf03(
1549 &when.condition,
1550 aliases,
1551 local_sources,
1552 statement_sources,
1553 dialect,
1554 has_outer_references,
1555 );
1556 let (rq, ru) = count_reference_qualification_in_expr_rf03(
1557 &when.result,
1558 aliases,
1559 local_sources,
1560 statement_sources,
1561 dialect,
1562 has_outer_references,
1563 );
1564 q += cq + rq;
1565 u += cu + ru;
1566 }
1567 if let Some(otherwise) = else_result {
1568 let (oq, ou) = count_reference_qualification_in_expr_rf03(
1569 otherwise,
1570 aliases,
1571 local_sources,
1572 statement_sources,
1573 dialect,
1574 has_outer_references,
1575 );
1576 q += oq;
1577 u += ou;
1578 }
1579 (q, u)
1580 }
1581 Expr::Function(function) => {
1582 let mut q = 0usize;
1583 let mut u = 0usize;
1584
1585 if let FunctionArguments::List(arguments) = &function.args {
1586 for (index, arg) in arguments.args.iter().enumerate() {
1587 match arg {
1588 FunctionArg::Unnamed(FunctionArgExpr::Expr(expr))
1589 | FunctionArg::Named {
1590 arg: FunctionArgExpr::Expr(expr),
1591 ..
1592 } => {
1593 if should_skip_identifier_reference_for_function_arg(
1594 function, index, expr,
1595 ) {
1596 continue;
1597 }
1598 let (aq, au) = count_reference_qualification_in_expr_rf03(
1599 expr,
1600 aliases,
1601 local_sources,
1602 statement_sources,
1603 dialect,
1604 has_outer_references,
1605 );
1606 q += aq;
1607 u += au;
1608 }
1609 _ => {}
1610 }
1611 }
1612 }
1613
1614 if let Some(filter) = &function.filter {
1615 let (fq, fu) = count_reference_qualification_in_expr_rf03(
1616 filter,
1617 aliases,
1618 local_sources,
1619 statement_sources,
1620 dialect,
1621 has_outer_references,
1622 );
1623 q += fq;
1624 u += fu;
1625 }
1626
1627 for order_expr in &function.within_group {
1628 let (oq, ou) = count_reference_qualification_in_expr_rf03(
1629 &order_expr.expr,
1630 aliases,
1631 local_sources,
1632 statement_sources,
1633 dialect,
1634 has_outer_references,
1635 );
1636 q += oq;
1637 u += ou;
1638 }
1639
1640 if let Some(WindowType::WindowSpec(spec)) = &function.over {
1641 for expr in &spec.partition_by {
1642 let (pq, pu) = count_reference_qualification_in_expr_rf03(
1643 expr,
1644 aliases,
1645 local_sources,
1646 statement_sources,
1647 dialect,
1648 has_outer_references,
1649 );
1650 q += pq;
1651 u += pu;
1652 }
1653 for order_expr in &spec.order_by {
1654 let (oq, ou) = count_reference_qualification_in_expr_rf03(
1655 &order_expr.expr,
1656 aliases,
1657 local_sources,
1658 statement_sources,
1659 dialect,
1660 has_outer_references,
1661 );
1662 q += oq;
1663 u += ou;
1664 }
1665 }
1666
1667 (q, u)
1668 }
1669 Expr::InSubquery { expr, .. } => count_reference_qualification_in_expr_rf03(
1670 expr,
1671 aliases,
1672 local_sources,
1673 statement_sources,
1674 dialect,
1675 has_outer_references,
1676 ),
1677 Expr::Exists { .. } | Expr::Subquery(_) => (0, 0),
1678 _ => (0, 0),
1679 }
1680}
1681
1682fn is_object_reference_dialect(dialect: Dialect) -> bool {
1683 matches!(
1684 dialect,
1685 Dialect::Bigquery | Dialect::Hive | Dialect::Redshift
1686 )
1687}
1688
1689fn should_skip_identifier_reference_for_function_arg(
1690 function: &sqlparser::ast::Function,
1691 arg_index: usize,
1692 expr: &Expr,
1693) -> bool {
1694 let Expr::Identifier(ident) = expr else {
1695 return false;
1696 };
1697 if ident.quote_style.is_some() || !is_date_part_identifier(&ident.value) {
1698 return false;
1699 }
1700
1701 let Some(function_name) = function_name_upper(function) else {
1702 return false;
1703 };
1704 if !is_datepart_function_name(&function_name) {
1705 return false;
1706 }
1707
1708 arg_index <= 1
1709}
1710
1711fn function_name_upper(function: &sqlparser::ast::Function) -> Option<String> {
1712 function
1713 .name
1714 .0
1715 .last()
1716 .and_then(sqlparser::ast::ObjectNamePart::as_ident)
1717 .map(|ident| ident.value.to_ascii_uppercase())
1718}
1719
1720fn is_datepart_function_name(name: &str) -> bool {
1721 matches!(
1722 name,
1723 "DATEDIFF"
1724 | "DATE_DIFF"
1725 | "DATEADD"
1726 | "DATE_ADD"
1727 | "DATE_PART"
1728 | "DATETIME_TRUNC"
1729 | "TIME_TRUNC"
1730 | "TIMESTAMP_TRUNC"
1731 | "TIMESTAMP_DIFF"
1732 | "TIMESTAMPDIFF"
1733 )
1734}
1735
1736fn is_date_part_identifier(value: &str) -> bool {
1737 matches!(
1738 value.to_ascii_uppercase().as_str(),
1739 "YEAR"
1740 | "QUARTER"
1741 | "MONTH"
1742 | "WEEK"
1743 | "DAY"
1744 | "DOW"
1745 | "DOY"
1746 | "HOUR"
1747 | "MINUTE"
1748 | "SECOND"
1749 | "MILLISECOND"
1750 | "MICROSECOND"
1751 | "NANOSECOND"
1752 )
1753}
1754
1755#[cfg(test)]
1756mod tests {
1757 use super::*;
1758 use crate::parser::parse_sql;
1759 use crate::types::IssueAutofixApplicability;
1760
1761 fn run(sql: &str) -> Vec<Issue> {
1762 let statements = parse_sql(sql).expect("parse");
1763 let rule = ReferencesConsistent::default();
1764 statements
1765 .iter()
1766 .enumerate()
1767 .flat_map(|(index, statement)| {
1768 rule.check(
1769 statement,
1770 &LintContext {
1771 sql,
1772 statement_range: 0..sql.len(),
1773 statement_index: index,
1774 },
1775 )
1776 })
1777 .collect()
1778 }
1779
1780 fn apply_issue_autofix(sql: &str, issue: &Issue) -> Option<String> {
1781 let autofix = issue.autofix.as_ref()?;
1782 let mut out = sql.to_string();
1783 let mut edits = autofix.edits.clone();
1784 edits.sort_by_key(|edit| (edit.span.start, edit.span.end));
1785 for edit in edits.into_iter().rev() {
1786 out.replace_range(edit.span.start..edit.span.end, &edit.replacement);
1787 }
1788 Some(out)
1789 }
1790
1791 fn apply_all_autofixes(sql: &str, issues: &[Issue]) -> String {
1792 let mut edits: Vec<_> = issues
1793 .iter()
1794 .filter_map(|issue| issue.autofix.as_ref())
1795 .flat_map(|autofix| autofix.edits.clone())
1796 .collect();
1797 edits.sort_by_key(|edit| (edit.span.start, edit.span.end));
1798 edits.dedup_by(|left, right| {
1799 left.span.start == right.span.start
1800 && left.span.end == right.span.end
1801 && left.replacement == right.replacement
1802 });
1803
1804 let mut out = sql.to_string();
1805 for edit in edits.into_iter().rev() {
1806 out.replace_range(edit.span.start..edit.span.end, &edit.replacement);
1807 }
1808 out
1809 }
1810
1811 #[test]
1814 fn flags_mixed_qualification_single_table() {
1815 let sql = "SELECT my_tbl.bar, baz FROM my_tbl";
1816 let issues = run(sql);
1817 assert_eq!(issues.len(), 2);
1818 assert!(issues
1819 .iter()
1820 .all(|issue| issue.code == issue_codes::LINT_RF_003));
1821 let issue_with_fix = issues
1822 .iter()
1823 .find(|issue| issue.autofix.is_some())
1824 .expect("issue with autofix");
1825 let autofix = issue_with_fix.autofix.as_ref().expect("autofix metadata");
1826 assert_eq!(autofix.applicability, IssueAutofixApplicability::Safe);
1827 let fixed = apply_issue_autofix(sql, issue_with_fix).expect("apply autofix");
1828 assert_eq!(fixed, "SELECT my_tbl.bar, my_tbl.baz FROM my_tbl");
1829 }
1830
1831 #[test]
1832 fn allows_consistently_unqualified_references() {
1833 let issues = run("SELECT bar FROM my_tbl");
1834 assert!(issues.is_empty());
1835 }
1836
1837 #[test]
1838 fn allows_consistently_qualified_references() {
1839 let issues = run("SELECT my_tbl.bar FROM my_tbl");
1840 assert!(issues.is_empty());
1841 }
1842
1843 #[test]
1844 fn flags_mixed_qualification_in_subquery() {
1845 let issues = run("SELECT * FROM (SELECT my_tbl.bar, baz FROM my_tbl)");
1846 assert_eq!(issues.len(), 2);
1847 }
1848
1849 #[test]
1850 fn allows_consistent_references_in_subquery() {
1851 let issues = run("SELECT * FROM (SELECT my_tbl.bar FROM my_tbl)");
1852 assert!(issues.is_empty());
1853 }
1854
1855 #[test]
1856 fn flags_mixed_qualification_with_qualified_wildcard() {
1857 let issues = run("SELECT my_tbl.*, bar FROM my_tbl");
1858 assert_eq!(issues.len(), 2);
1859 }
1860
1861 #[test]
1862 fn allows_consistent_qualified_wildcard_and_columns() {
1863 let issues = run("SELECT my_tbl.*, my_tbl.bar FROM my_tbl");
1864 assert!(issues.is_empty());
1865 }
1866
1867 #[test]
1868 fn qualified_mode_flags_unqualified_references() {
1869 let config = LintConfig {
1870 enabled: true,
1871 disabled_rules: vec![],
1872 rule_configs: std::collections::BTreeMap::from([(
1873 "references.consistent".to_string(),
1874 serde_json::json!({"single_table_references": "qualified"}),
1875 )]),
1876 };
1877 let rule = ReferencesConsistent::from_config(&config);
1878 let sql = "SELECT bar FROM my_tbl";
1879 let statements = parse_sql(sql).expect("parse");
1880 let issues = rule.check(
1881 &statements[0],
1882 &LintContext {
1883 sql,
1884 statement_range: 0..sql.len(),
1885 statement_index: 0,
1886 },
1887 );
1888 assert_eq!(issues.len(), 1);
1889 }
1890
1891 #[test]
1892 fn force_enable_false_disables_rule() {
1893 let config = LintConfig {
1894 enabled: true,
1895 disabled_rules: vec![],
1896 rule_configs: std::collections::BTreeMap::from([(
1897 "LINT_RF_003".to_string(),
1898 serde_json::json!({"force_enable": false}),
1899 )]),
1900 };
1901 let rule = ReferencesConsistent::from_config(&config);
1902 let sql = "SELECT my_tbl.bar, baz FROM my_tbl";
1903 let statements = parse_sql(sql).expect("parse");
1904 let issues = rule.check(
1905 &statements[0],
1906 &LintContext {
1907 sql,
1908 statement_range: 0..sql.len(),
1909 statement_index: 0,
1910 },
1911 );
1912 assert!(issues.is_empty());
1913 }
1914
1915 #[test]
1916 fn autofix_uses_document_spans_for_trimmed_statement_ranges() {
1917 let sql = "-- c1\n-- c2\n-- c3\n-- c4\n-- c5\n-- c6\n-- c7\n-- c8\n-- c9\n-- c10\n-- c11\n-- c12\nSELECT\n t.a,\n b,\n c,\n d,\n e,\n f,\n g,\n h,\n i,\n j,\n k,\n l,\n m,\n n\nFROM foo AS t";
1918 let statements = parse_sql(sql).expect("parse");
1919 let statement_start = sql.find("SELECT").expect("statement start");
1920 let statement_end = sql.len();
1921 let rule = ReferencesConsistent::default();
1922
1923 let issues = rule.check(
1924 &statements[0],
1925 &LintContext {
1926 sql,
1927 statement_range: statement_start..statement_end,
1928 statement_index: 0,
1929 },
1930 );
1931
1932 let fixed = apply_all_autofixes(sql, &issues);
1933 assert!(fixed.contains(" t.b,"));
1934 assert!(fixed.contains(" t.n"));
1935 assert!(fixed.contains("FROM foo AS t"));
1936 assert!(!fixed.contains("\n b,"));
1937 }
1938}