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 CreateView, Expr, FromTable, Ident, Merge, Query, SetExpr, Statement, TableFactor,
10 TableWithJoins, Update, UpdateTableFromKind,
11};
12use sqlparser::tokenizer::{Token, TokenWithSpan, Tokenizer, Whitespace};
13
14#[derive(Clone, Copy, Debug, Eq, PartialEq)]
15enum AliasingPreference {
16 Explicit,
17 Implicit,
18}
19
20impl AliasingPreference {
21 fn from_config(config: &LintConfig, rule_code: &str) -> Self {
22 match config
23 .rule_option_str(rule_code, "aliasing")
24 .unwrap_or("explicit")
25 .to_ascii_lowercase()
26 .as_str()
27 {
28 "implicit" => Self::Implicit,
29 _ => Self::Explicit,
30 }
31 }
32
33 fn message(self) -> &'static str {
34 match self {
35 Self::Explicit => "Use explicit AS when aliasing tables.",
36 Self::Implicit => "Use implicit aliasing when aliasing tables (omit AS).",
37 }
38 }
39
40 fn violation(self, explicit_as: bool) -> bool {
41 match self {
42 Self::Explicit => !explicit_as,
43 Self::Implicit => explicit_as,
44 }
45 }
46}
47
48pub struct AliasingTableStyle {
49 aliasing: AliasingPreference,
50}
51
52impl AliasingTableStyle {
53 pub fn from_config(config: &LintConfig) -> Self {
54 Self {
55 aliasing: AliasingPreference::from_config(config, issue_codes::LINT_AL_001),
56 }
57 }
58}
59
60impl Default for AliasingTableStyle {
61 fn default() -> Self {
62 Self {
63 aliasing: AliasingPreference::Explicit,
64 }
65 }
66}
67
68impl LintRule for AliasingTableStyle {
69 fn code(&self) -> &'static str {
70 issue_codes::LINT_AL_001
71 }
72
73 fn name(&self) -> &'static str {
74 "Table alias style"
75 }
76
77 fn description(&self) -> &'static str {
78 "Implicit/explicit aliasing of table."
79 }
80
81 fn check(&self, statement: &Statement, ctx: &LintContext) -> Vec<Issue> {
82 let mut issues = Vec::new();
83 let tokens =
84 tokenized_for_context(ctx).or_else(|| tokenized(ctx.statement_sql(), ctx.dialect()));
85
86 collect_table_aliases_in_statement(statement, &mut |alias| {
87 let Some(occurrence) = alias_occurrence_in_statement(alias, ctx, tokens.as_deref())
88 else {
89 return;
90 };
91
92 if !self.aliasing.violation(occurrence.explicit_as) {
93 return;
94 }
95
96 let mut issue = Issue::warning(issue_codes::LINT_AL_001, self.aliasing.message())
97 .with_statement(ctx.statement_index)
98 .with_span(ctx.span_from_statement_offset(occurrence.start, occurrence.end));
99 if let Some(edits) = autofix_edits_for_occurrence(occurrence, self.aliasing) {
100 issue = issue.with_autofix_edits(IssueAutofixApplicability::Safe, edits);
101 }
102
103 issues.push(issue);
104 });
105
106 issues
107 }
108}
109
110#[derive(Clone, Copy)]
111struct AliasOccurrence {
112 start: usize,
113 end: usize,
114 explicit_as: bool,
115 as_span: Option<Span>,
116 has_leading_whitespace: bool,
118}
119
120fn autofix_edits_for_occurrence(
121 occurrence: AliasOccurrence,
122 aliasing: AliasingPreference,
123) -> Option<Vec<IssuePatchEdit>> {
124 match aliasing {
125 AliasingPreference::Explicit if !occurrence.explicit_as => {
126 let insert = Span::new(occurrence.start, occurrence.start);
127 let replacement = if occurrence.has_leading_whitespace {
128 "AS "
129 } else {
130 " AS "
131 };
132 Some(vec![IssuePatchEdit::new(insert, replacement)])
133 }
134 AliasingPreference::Implicit if occurrence.explicit_as => {
135 let as_span = occurrence.as_span?;
136 let delete_end = occurrence.start;
139 Some(vec![IssuePatchEdit::new(
140 Span::new(as_span.start, delete_end),
141 " ",
142 )])
143 }
144 _ => None,
145 }
146}
147
148fn alias_occurrence_in_statement(
149 alias: &Ident,
150 ctx: &LintContext,
151 tokens: Option<&[LocatedToken]>,
152) -> Option<AliasOccurrence> {
153 let tokens = tokens?;
154
155 let abs_start = line_col_to_offset(
156 ctx.sql,
157 alias.span.start.line as usize,
158 alias.span.start.column as usize,
159 )?;
160 let abs_end = line_col_to_offset(
161 ctx.sql,
162 alias.span.end.line as usize,
163 alias.span.end.column as usize,
164 )?;
165
166 if abs_start < ctx.statement_range.start || abs_end > ctx.statement_range.end {
167 return None;
168 }
169
170 let rel_start = abs_start - ctx.statement_range.start;
171 let rel_end = abs_end - ctx.statement_range.start;
172 let (explicit_as, as_span) = explicit_as_before_alias_tokens(tokens, rel_start)?;
173 let has_leading_whitespace = has_whitespace_before(tokens, rel_start);
174 Some(AliasOccurrence {
175 start: rel_start,
176 end: rel_end,
177 explicit_as,
178 as_span,
179 has_leading_whitespace,
180 })
181}
182
183fn collect_table_aliases_in_statement<F: FnMut(&Ident)>(statement: &Statement, visitor: &mut F) {
184 match statement {
185 Statement::Query(query) => collect_table_aliases_in_query(query, visitor),
186 Statement::Insert(insert) => {
187 if let Some(source) = &insert.source {
188 collect_table_aliases_in_query(source, visitor);
189 }
190 }
191 Statement::CreateView(CreateView { query, .. }) => {
192 collect_table_aliases_in_query(query, visitor)
193 }
194 Statement::CreateTable(create) => {
195 if let Some(query) = &create.query {
196 collect_table_aliases_in_query(query, visitor);
197 }
198 }
199 Statement::Update(Update {
200 table,
201 from,
202 selection,
203 ..
204 }) => {
205 for join in &table.joins {
209 collect_table_aliases_in_table_factor(&join.relation, visitor);
210 }
211 if let Some(from) = from {
212 match from {
213 UpdateTableFromKind::BeforeSet(tables)
214 | UpdateTableFromKind::AfterSet(tables) => {
215 for t in tables {
216 collect_table_aliases_in_table_with_joins(t, visitor);
217 }
218 }
219 }
220 }
221 if let Some(selection) = selection {
222 collect_table_aliases_in_expr(selection, visitor);
223 }
224 }
225 Statement::Delete(delete) => {
226 match &delete.from {
227 FromTable::WithFromKeyword(tables) | FromTable::WithoutKeyword(tables) => {
228 for t in tables {
229 collect_table_aliases_in_table_with_joins(t, visitor);
230 }
231 }
232 }
233 if let Some(using) = &delete.using {
234 for t in using {
235 collect_table_aliases_in_table_with_joins(t, visitor);
236 }
237 }
238 if let Some(selection) = &delete.selection {
239 collect_table_aliases_in_expr(selection, visitor);
240 }
241 }
242 Statement::Merge(Merge { table, source, .. }) => {
243 collect_table_aliases_in_table_factor(table, visitor);
244 collect_table_aliases_in_table_factor(source, visitor);
245 }
246 _ => {}
247 }
248}
249
250fn collect_table_aliases_in_query<F: FnMut(&Ident)>(query: &Query, visitor: &mut F) {
251 if let Some(with) = &query.with {
252 for cte in &with.cte_tables {
253 collect_table_aliases_in_query(&cte.query, visitor);
254 }
255 }
256
257 collect_table_aliases_in_set_expr(&query.body, visitor);
258}
259
260fn collect_table_aliases_in_set_expr<F: FnMut(&Ident)>(set_expr: &SetExpr, visitor: &mut F) {
261 match set_expr {
262 SetExpr::Select(select) => {
263 for table in &select.from {
264 collect_table_aliases_in_table_with_joins(table, visitor);
265 for join in &table.joins {
267 if let Some(expr) = join_constraint_expr(&join.join_operator) {
268 collect_table_aliases_in_expr(expr, visitor);
269 }
270 }
271 }
272 if let Some(selection) = &select.selection {
273 collect_table_aliases_in_expr(selection, visitor);
274 }
275 if let Some(having) = &select.having {
276 collect_table_aliases_in_expr(having, visitor);
277 }
278 if let Some(qualify) = &select.qualify {
279 collect_table_aliases_in_expr(qualify, visitor);
280 }
281 for item in &select.projection {
283 match item {
284 sqlparser::ast::SelectItem::UnnamedExpr(expr)
285 | sqlparser::ast::SelectItem::ExprWithAlias { expr, .. } => {
286 collect_table_aliases_in_expr(expr, visitor);
287 }
288 _ => {}
289 }
290 }
291 }
292 SetExpr::Query(query) => collect_table_aliases_in_query(query, visitor),
293 SetExpr::SetOperation { left, right, .. } => {
294 collect_table_aliases_in_set_expr(left, visitor);
295 collect_table_aliases_in_set_expr(right, visitor);
296 }
297 SetExpr::Insert(statement)
298 | SetExpr::Update(statement)
299 | SetExpr::Delete(statement)
300 | SetExpr::Merge(statement) => collect_table_aliases_in_statement(statement, visitor),
301 _ => {}
302 }
303}
304
305fn collect_table_aliases_in_table_with_joins<F: FnMut(&Ident)>(
306 table_with_joins: &TableWithJoins,
307 visitor: &mut F,
308) {
309 collect_table_aliases_in_table_factor(&table_with_joins.relation, visitor);
310 for join in &table_with_joins.joins {
311 collect_table_aliases_in_table_factor(&join.relation, visitor);
312 }
313}
314
315fn collect_table_aliases_in_table_factor<F: FnMut(&Ident)>(
316 table_factor: &TableFactor,
317 visitor: &mut F,
318) {
319 if let Some(alias) = table_factor_alias_ident(table_factor) {
320 visitor(alias);
321 }
322
323 match table_factor {
324 TableFactor::Derived { subquery, .. } => collect_table_aliases_in_query(subquery, visitor),
325 TableFactor::NestedJoin {
326 table_with_joins, ..
327 } => collect_table_aliases_in_table_with_joins(table_with_joins, visitor),
328 TableFactor::Pivot { table, .. }
329 | TableFactor::Unpivot { table, .. }
330 | TableFactor::MatchRecognize { table, .. } => {
331 collect_table_aliases_in_table_factor(table, visitor)
332 }
333 _ => {}
334 }
335}
336
337fn collect_table_aliases_in_expr<F: FnMut(&Ident)>(expr: &Expr, visitor: &mut F) {
339 match expr {
340 Expr::Subquery(query)
341 | Expr::Exists {
342 subquery: query, ..
343 } => {
344 collect_table_aliases_in_query(query, visitor);
345 }
346 Expr::InSubquery {
347 expr: inner,
348 subquery,
349 ..
350 } => {
351 collect_table_aliases_in_expr(inner, visitor);
352 collect_table_aliases_in_query(subquery, visitor);
353 }
354 Expr::BinaryOp { left, right, .. } => {
355 collect_table_aliases_in_expr(left, visitor);
356 collect_table_aliases_in_expr(right, visitor);
357 }
358 Expr::UnaryOp { expr: inner, .. }
359 | Expr::Nested(inner)
360 | Expr::Cast { expr: inner, .. } => {
361 collect_table_aliases_in_expr(inner, visitor);
362 }
363 Expr::Case {
364 operand,
365 conditions,
366 else_result,
367 ..
368 } => {
369 if let Some(op) = operand {
370 collect_table_aliases_in_expr(op, visitor);
371 }
372 for cw in conditions {
373 collect_table_aliases_in_expr(&cw.condition, visitor);
374 collect_table_aliases_in_expr(&cw.result, visitor);
375 }
376 if let Some(el) = else_result {
377 collect_table_aliases_in_expr(el, visitor);
378 }
379 }
380 Expr::Function(func) => {
381 if let sqlparser::ast::FunctionArguments::List(arg_list) = &func.args {
382 for arg in &arg_list.args {
383 match arg {
384 sqlparser::ast::FunctionArg::Unnamed(
385 sqlparser::ast::FunctionArgExpr::Expr(e),
386 )
387 | sqlparser::ast::FunctionArg::Named {
388 arg: sqlparser::ast::FunctionArgExpr::Expr(e),
389 ..
390 } => collect_table_aliases_in_expr(e, visitor),
391 _ => {}
392 }
393 }
394 } else if let sqlparser::ast::FunctionArguments::Subquery(query) = &func.args {
395 collect_table_aliases_in_query(query, visitor);
396 }
397 }
398 Expr::Between {
399 expr: inner,
400 low,
401 high,
402 ..
403 } => {
404 collect_table_aliases_in_expr(inner, visitor);
405 collect_table_aliases_in_expr(low, visitor);
406 collect_table_aliases_in_expr(high, visitor);
407 }
408 Expr::InList {
409 expr: inner, list, ..
410 } => {
411 collect_table_aliases_in_expr(inner, visitor);
412 for item in list {
413 collect_table_aliases_in_expr(item, visitor);
414 }
415 }
416 Expr::IsNull(inner) | Expr::IsNotNull(inner) => {
417 collect_table_aliases_in_expr(inner, visitor);
418 }
419 _ => {}
420 }
421}
422
423fn join_constraint_expr(op: &sqlparser::ast::JoinOperator) -> Option<&Expr> {
424 use sqlparser::ast::{JoinConstraint, JoinOperator};
425 let constraint = match op {
426 JoinOperator::Join(c)
427 | JoinOperator::Inner(c)
428 | JoinOperator::Left(c)
429 | JoinOperator::LeftOuter(c)
430 | JoinOperator::Right(c)
431 | JoinOperator::RightOuter(c)
432 | JoinOperator::FullOuter(c)
433 | JoinOperator::CrossJoin(c)
434 | JoinOperator::Semi(c)
435 | JoinOperator::LeftSemi(c)
436 | JoinOperator::RightSemi(c)
437 | JoinOperator::Anti(c)
438 | JoinOperator::LeftAnti(c)
439 | JoinOperator::RightAnti(c)
440 | JoinOperator::StraightJoin(c) => c,
441 JoinOperator::AsOf { constraint, .. } => constraint,
442 JoinOperator::CrossApply | JoinOperator::OuterApply => return None,
443 };
444 if let JoinConstraint::On(expr) = constraint {
445 Some(expr)
446 } else {
447 None
448 }
449}
450
451fn table_factor_alias_ident(table_factor: &TableFactor) -> Option<&Ident> {
452 let alias = match table_factor {
453 TableFactor::Table { alias, .. }
454 | TableFactor::Derived { alias, .. }
455 | TableFactor::TableFunction { alias, .. }
456 | TableFactor::Function { alias, .. }
457 | TableFactor::UNNEST { alias, .. }
458 | TableFactor::JsonTable { alias, .. }
459 | TableFactor::OpenJsonTable { alias, .. }
460 | TableFactor::NestedJoin { alias, .. }
461 | TableFactor::Pivot { alias, .. }
462 | TableFactor::Unpivot { alias, .. }
463 | TableFactor::MatchRecognize { alias, .. }
464 | TableFactor::XmlTable { alias, .. }
465 | TableFactor::SemanticView { alias, .. } => alias.as_ref(),
466 }?;
467
468 Some(&alias.name)
469}
470
471fn explicit_as_before_alias_tokens(
472 tokens: &[LocatedToken],
473 alias_start: usize,
474) -> Option<(bool, Option<Span>)> {
475 let token = tokens
476 .iter()
477 .rev()
478 .find(|token| token.end <= alias_start && !is_trivia_token(&token.token))?;
479 if is_as_token(&token.token) {
480 let leading_ws_start = tokens
482 .iter()
483 .rev()
484 .find(|t| t.end <= token.start && !is_trivia_token(&t.token))
485 .map(|t| t.end)
486 .unwrap_or(token.start);
487 Some((true, Some(Span::new(leading_ws_start, token.end))))
488 } else {
489 Some((false, None))
490 }
491}
492
493fn has_whitespace_before(tokens: &[LocatedToken], pos: usize) -> bool {
495 tokens
496 .iter()
497 .rev()
498 .find(|t| t.end <= pos)
499 .is_some_and(|t| is_trivia_token(&t.token))
500}
501
502fn is_as_token(token: &Token) -> bool {
503 match token {
504 Token::Word(word) => word.value.eq_ignore_ascii_case("AS"),
505 _ => false,
506 }
507}
508
509#[derive(Clone)]
510struct LocatedToken {
511 token: Token,
512 start: usize,
513 end: usize,
514}
515
516fn tokenized(sql: &str, dialect: Dialect) -> Option<Vec<LocatedToken>> {
517 let dialect = dialect.to_sqlparser_dialect();
518 let mut tokenizer = Tokenizer::new(dialect.as_ref(), sql);
519 let tokens = tokenizer.tokenize_with_location().ok()?;
520
521 let mut out = Vec::with_capacity(tokens.len());
522 for token in tokens {
523 let (start, end) = token_with_span_offsets(sql, &token)?;
524 out.push(LocatedToken {
525 token: token.token,
526 start,
527 end,
528 });
529 }
530 Some(out)
531}
532
533fn tokenized_for_context(ctx: &LintContext) -> Option<Vec<LocatedToken>> {
534 let statement_start = ctx.statement_range.start;
535 ctx.with_document_tokens(|tokens| {
536 if tokens.is_empty() {
537 return None;
538 }
539
540 Some(
541 tokens
542 .iter()
543 .filter_map(|token| {
544 let (start, end) = token_with_span_offsets(ctx.sql, token)?;
545 if start < ctx.statement_range.start || end > ctx.statement_range.end {
546 return None;
547 }
548 Some(LocatedToken {
549 token: token.token.clone(),
550 start: start - statement_start,
551 end: end - statement_start,
552 })
553 })
554 .collect::<Vec<_>>(),
555 )
556 })
557}
558
559fn token_with_span_offsets(sql: &str, token: &TokenWithSpan) -> Option<(usize, usize)> {
560 let start = line_col_to_offset(
561 sql,
562 token.span.start.line as usize,
563 token.span.start.column as usize,
564 )?;
565 let end = line_col_to_offset(
566 sql,
567 token.span.end.line as usize,
568 token.span.end.column as usize,
569 )?;
570 Some((start, end))
571}
572
573fn is_trivia_token(token: &Token) -> bool {
574 matches!(
575 token,
576 Token::Whitespace(Whitespace::Space | Whitespace::Tab | Whitespace::Newline)
577 | Token::Whitespace(Whitespace::SingleLineComment { .. })
578 | Token::Whitespace(Whitespace::MultiLineComment(_))
579 )
580}
581
582fn line_col_to_offset(sql: &str, line: usize, column: usize) -> Option<usize> {
583 if line == 0 || column == 0 {
584 return None;
585 }
586
587 let mut current_line = 1usize;
588 let mut current_col = 1usize;
589
590 for (offset, ch) in sql.char_indices() {
591 if current_line == line && current_col == column {
592 return Some(offset);
593 }
594
595 if ch == '\n' {
596 current_line += 1;
597 current_col = 1;
598 } else {
599 current_col += 1;
600 }
601 }
602
603 if current_line == line && current_col == column {
604 return Some(sql.len());
605 }
606
607 None
608}
609
610#[cfg(test)]
611mod tests {
612 use super::*;
613 use crate::{
614 parser::{parse_sql, parse_sql_with_dialect},
615 types::IssueAutofixApplicability,
616 Dialect,
617 };
618
619 fn run_with_rule(sql: &str, rule: AliasingTableStyle) -> Vec<Issue> {
620 let stmts = parse_sql(sql).expect("parse");
621 stmts
622 .iter()
623 .enumerate()
624 .flat_map(|(index, stmt)| {
625 rule.check(
626 stmt,
627 &LintContext {
628 sql,
629 statement_range: 0..sql.len(),
630 statement_index: index,
631 },
632 )
633 })
634 .collect()
635 }
636
637 fn run(sql: &str) -> Vec<Issue> {
638 run_with_rule(sql, AliasingTableStyle::default())
639 }
640
641 #[test]
642 fn flags_implicit_table_aliases() {
643 let issues = run("select * from users u join orders o on u.id = o.user_id");
644 assert_eq!(issues.len(), 2);
645 assert!(issues.iter().all(|i| i.code == issue_codes::LINT_AL_001));
646 }
647
648 #[test]
649 fn allows_explicit_as_table_aliases() {
650 let issues = run("select * from users as u join orders as o on u.id = o.user_id");
651 assert!(issues.is_empty());
652 }
653
654 #[test]
655 fn flags_explicit_aliases_when_implicit_policy_requested() {
656 let config = LintConfig {
657 enabled: true,
658 disabled_rules: vec![],
659 rule_configs: std::collections::BTreeMap::from([(
660 "LINT_AL_001".to_string(),
661 serde_json::json!({"aliasing": "implicit"}),
662 )]),
663 };
664 let issues = run_with_rule(
665 "select * from users as u join orders as o on u.id = o.user_id",
666 AliasingTableStyle::from_config(&config),
667 );
668 assert_eq!(issues.len(), 2);
669 }
670
671 #[test]
672 fn flags_implicit_derived_table_alias() {
673 let issues = run("select * from (select 1) d");
674 assert_eq!(issues.len(), 1);
675 assert_eq!(issues[0].code, issue_codes::LINT_AL_001);
676 }
677
678 #[test]
679 fn flags_implicit_merge_aliases_in_bigquery() {
680 let sql = "MERGE dataset.inventory t USING dataset.newarrivals s ON t.product = s.product WHEN MATCHED THEN UPDATE SET quantity = t.quantity + s.quantity";
681 let statements = parse_sql_with_dialect(sql, Dialect::Bigquery).expect("parse");
682 let issues = statements
683 .iter()
684 .enumerate()
685 .flat_map(|(index, stmt)| {
686 AliasingTableStyle::default().check(
687 stmt,
688 &LintContext {
689 sql,
690 statement_range: 0..sql.len(),
691 statement_index: index,
692 },
693 )
694 })
695 .collect::<Vec<_>>();
696 assert_eq!(issues.len(), 2);
697 assert!(issues.iter().all(|i| i.code == issue_codes::LINT_AL_001));
698 }
699
700 #[test]
701 fn explicit_mode_emits_safe_insert_as_autofix_patch() {
702 let sql = "select * from users u";
703 let issues = run(sql);
704 assert_eq!(issues.len(), 1);
705
706 let autofix = issues[0]
707 .autofix
708 .as_ref()
709 .expect("expected AL001 core autofix metadata in explicit mode");
710 assert_eq!(autofix.applicability, IssueAutofixApplicability::Safe);
711 assert_eq!(autofix.edits.len(), 1);
712 assert_eq!(autofix.edits[0].replacement, "AS ");
713 assert_eq!(autofix.edits[0].span.start, autofix.edits[0].span.end);
714 }
715
716 #[test]
717 fn implicit_mode_emits_safe_remove_as_autofix_patch() {
718 let config = LintConfig {
719 enabled: true,
720 disabled_rules: vec![],
721 rule_configs: std::collections::BTreeMap::from([(
722 "LINT_AL_001".to_string(),
723 serde_json::json!({"aliasing": "implicit"}),
724 )]),
725 };
726 let rule = AliasingTableStyle::from_config(&config);
727 let sql = "select * from users as u";
728 let issues = run_with_rule(sql, rule);
729 assert_eq!(issues.len(), 1);
730
731 let autofix = issues[0]
732 .autofix
733 .as_ref()
734 .expect("expected AL001 core autofix metadata in implicit mode");
735 assert_eq!(autofix.applicability, IssueAutofixApplicability::Safe);
736 assert_eq!(autofix.edits.len(), 1);
737 assert_eq!(autofix.edits[0].replacement, " ");
738 assert_eq!(
740 &sql[autofix.edits[0].span.start..autofix.edits[0].span.end],
741 " as "
742 );
743 }
744
745 #[test]
746 fn flags_implicit_aliases_in_update_where_exists_subquery() {
747 let sql = "UPDATE t SET x = 1 WHERE EXISTS (SELECT 1 FROM users u)";
748 let issues = run(sql);
749 assert_eq!(issues.len(), 1);
751 assert_eq!(issues[0].code, issue_codes::LINT_AL_001);
752 }
753
754 #[test]
755 fn skips_update_target_table_alias() {
756 let sql = "UPDATE users u SET u.x = 1";
758 let issues = run(sql);
759 assert!(issues.is_empty());
760 }
761
762 #[test]
763 fn flags_implicit_aliases_in_delete_where_exists() {
764 let sql =
765 "DELETE FROM users u WHERE EXISTS (SELECT 1 FROM orders o WHERE o.user_id = u.id)";
766 let issues = run(sql);
767 assert_eq!(issues.len(), 2);
769 }
770}