1use rowan::{TextRange, TextSize};
2use squawk_linter::Edit;
3use squawk_syntax::{
4 SyntaxKind,
5 ast::{self, AstNode},
6};
7
8use crate::{
9 column_name::ColumnName,
10 offsets::token_from_offset,
11 quote::{quote_column_alias, unquote_ident},
12 symbols::Name,
13};
14
15#[derive(Debug, Clone)]
16pub enum ActionKind {
17 QuickFix,
18 RefactorRewrite,
19}
20
21#[derive(Debug, Clone)]
22pub struct CodeAction {
23 pub title: String,
24 pub edits: Vec<Edit>,
25 pub kind: ActionKind,
26}
27
28pub fn code_actions(file: ast::SourceFile, offset: TextSize) -> Option<Vec<CodeAction>> {
29 let mut actions = vec![];
30 rewrite_as_regular_string(&mut actions, &file, offset);
31 rewrite_as_dollar_quoted_string(&mut actions, &file, offset);
32 remove_else_clause(&mut actions, &file, offset);
33 rewrite_table_as_select(&mut actions, &file, offset);
34 rewrite_select_as_table(&mut actions, &file, offset);
35 quote_identifier(&mut actions, &file, offset);
36 unquote_identifier(&mut actions, &file, offset);
37 add_explicit_alias(&mut actions, &file, offset);
38 remove_redundant_alias(&mut actions, &file, offset);
39 Some(actions)
40}
41
42fn rewrite_as_regular_string(
43 actions: &mut Vec<CodeAction>,
44 file: &ast::SourceFile,
45 offset: TextSize,
46) -> Option<()> {
47 let dollar_string = file
48 .syntax()
49 .token_at_offset(offset)
50 .find(|token| token.kind() == SyntaxKind::DOLLAR_QUOTED_STRING)?;
51
52 let replacement = dollar_quoted_to_string(dollar_string.text())?;
53 actions.push(CodeAction {
54 title: "Rewrite as regular string".to_owned(),
55 edits: vec![Edit::replace(dollar_string.text_range(), replacement)],
56 kind: ActionKind::RefactorRewrite,
57 });
58
59 Some(())
60}
61
62fn rewrite_as_dollar_quoted_string(
63 actions: &mut Vec<CodeAction>,
64 file: &ast::SourceFile,
65 offset: TextSize,
66) -> Option<()> {
67 let string = file
68 .syntax()
69 .token_at_offset(offset)
70 .find(|token| token.kind() == SyntaxKind::STRING)?;
71
72 let replacement = string_to_dollar_quoted(string.text())?;
73 actions.push(CodeAction {
74 title: "Rewrite as dollar-quoted string".to_owned(),
75 edits: vec![Edit::replace(string.text_range(), replacement)],
76 kind: ActionKind::RefactorRewrite,
77 });
78
79 Some(())
80}
81
82fn string_to_dollar_quoted(text: &str) -> Option<String> {
83 let normalized = normalize_single_quoted_string(text)?;
84 let delimiter = dollar_delimiter(&normalized)?;
85 let boundary = format!("${}$", delimiter);
86 Some(format!("{boundary}{normalized}{boundary}"))
87}
88
89fn dollar_quoted_to_string(text: &str) -> Option<String> {
90 debug_assert!(text.starts_with('$'));
91 let (delimiter, content) = split_dollar_quoted(text)?;
92 let boundary = format!("${}$", delimiter);
93
94 if !text.starts_with(&boundary) || !text.ends_with(&boundary) {
95 return None;
96 }
97
98 let escaped = content.replace('\'', "''");
100 Some(format!("'{}'", escaped))
101}
102
103fn split_dollar_quoted(text: &str) -> Option<(String, &str)> {
104 debug_assert!(text.starts_with('$'));
105 let second_dollar = text[1..].find('$')?;
106 let delimiter = &text[1..=second_dollar];
108 let boundary = format!("${}$", delimiter);
109
110 if !text.ends_with(&boundary) {
111 return None;
112 }
113
114 let start = boundary.len();
115 let end = text.len().checked_sub(boundary.len())?;
116 let content = text.get(start..end)?;
117 Some((delimiter.to_owned(), content))
118}
119
120fn normalize_single_quoted_string(text: &str) -> Option<String> {
121 let body = text.strip_prefix('\'')?.strip_suffix('\'')?;
122 return Some(body.replace("''", "'"));
123}
124
125fn dollar_delimiter(content: &str) -> Option<String> {
126 if !content.contains("$$") && !content.ends_with('$') {
129 return Some("".to_owned());
130 }
131
132 let mut delim = "q".to_owned();
133 for idx in 0..10 {
135 if !content.contains(&format!("${}$", delim)) {
136 return Some(delim);
137 }
138 delim.push_str(&idx.to_string());
139 }
140 None
141}
142
143fn remove_else_clause(
144 actions: &mut Vec<CodeAction>,
145 file: &ast::SourceFile,
146 offset: TextSize,
147) -> Option<()> {
148 let else_token = file
149 .syntax()
150 .token_at_offset(offset)
151 .find(|x| x.kind() == SyntaxKind::ELSE_KW)?;
152 let parent = else_token.parent()?;
153 let else_clause = ast::ElseClause::cast(parent)?;
154
155 let mut edits = vec![];
156 edits.push(Edit::delete(else_clause.syntax().text_range()));
157 if let Some(token) = else_token.prev_token() {
158 if token.kind() == SyntaxKind::WHITESPACE {
159 edits.push(Edit::delete(token.text_range()));
160 }
161 }
162
163 actions.push(CodeAction {
164 title: "Remove `else` clause".to_owned(),
165 edits,
166 kind: ActionKind::RefactorRewrite,
167 });
168 Some(())
169}
170
171fn rewrite_table_as_select(
172 actions: &mut Vec<CodeAction>,
173 file: &ast::SourceFile,
174 offset: TextSize,
175) -> Option<()> {
176 let token = token_from_offset(file, offset)?;
177 let table = token.parent_ancestors().find_map(ast::Table::cast)?;
178
179 let relation_name = table.relation_name()?;
180 let table_name = relation_name.syntax().text();
181
182 let replacement = format!("select * from {}", table_name);
183
184 actions.push(CodeAction {
185 title: "Rewrite as `select`".to_owned(),
186 edits: vec![Edit::replace(table.syntax().text_range(), replacement)],
187 kind: ActionKind::RefactorRewrite,
188 });
189
190 Some(())
191}
192
193fn rewrite_select_as_table(
194 actions: &mut Vec<CodeAction>,
195 file: &ast::SourceFile,
196 offset: TextSize,
197) -> Option<()> {
198 let token = token_from_offset(file, offset)?;
199 let select = token.parent_ancestors().find_map(ast::Select::cast)?;
200
201 if !can_transform_select_to_table(&select) {
202 return None;
203 }
204
205 let from_clause = select.from_clause()?;
206 let from_item = from_clause.from_items().next()?;
207
208 let table_name = if let Some(name_ref) = from_item.name_ref() {
209 name_ref.syntax().text().to_string()
210 } else if let Some(field_expr) = from_item.field_expr() {
211 field_expr.syntax().text().to_string()
212 } else {
213 return None;
214 };
215
216 let replacement = format!("table {}", table_name);
217
218 actions.push(CodeAction {
219 title: "Rewrite as `table`".to_owned(),
220 edits: vec![Edit::replace(select.syntax().text_range(), replacement)],
221 kind: ActionKind::RefactorRewrite,
222 });
223
224 Some(())
225}
226
227fn can_transform_select_to_table(select: &ast::Select) -> bool {
234 if select.with_clause().is_some()
235 || select.where_clause().is_some()
236 || select.group_by_clause().is_some()
237 || select.having_clause().is_some()
238 || select.window_clause().is_some()
239 || select.order_by_clause().is_some()
240 || select.limit_clause().is_some()
241 || select.fetch_clause().is_some()
242 || select.offset_clause().is_some()
243 || select.filter_clause().is_some()
244 || select.locking_clauses().next().is_some()
245 {
246 return false;
247 }
248
249 let Some(select_clause) = select.select_clause() else {
250 return false;
251 };
252
253 if select_clause.distinct_clause().is_some() {
254 return false;
255 }
256
257 let Some(target_list) = select_clause.target_list() else {
258 return false;
259 };
260
261 let mut targets = target_list.targets();
262 let Some(target) = targets.next() else {
263 return false;
264 };
265
266 if targets.next().is_some() {
267 return false;
268 }
269
270 if target.expr().is_some() || target.star_token().is_none() {
272 return false;
273 }
274
275 let Some(from_clause) = select.from_clause() else {
276 return false;
277 };
278
279 let mut from_items = from_clause.from_items();
280 let Some(from_item) = from_items.next() else {
281 return false;
282 };
283
284 if from_items.next().is_some() || from_clause.join_exprs().next().is_some() {
286 return false;
287 }
288
289 if from_item.alias().is_some()
290 || from_item.tablesample_clause().is_some()
291 || from_item.only_token().is_some()
292 || from_item.lateral_token().is_some()
293 || from_item.star_token().is_some()
294 || from_item.call_expr().is_some()
295 || from_item.paren_select().is_some()
296 || from_item.json_table().is_some()
297 || from_item.xml_table().is_some()
298 || from_item.cast_expr().is_some()
299 {
300 return false;
301 }
302
303 from_item.name_ref().is_some() || from_item.field_expr().is_some()
305}
306
307fn quote_identifier(
308 actions: &mut Vec<CodeAction>,
309 file: &ast::SourceFile,
310 offset: TextSize,
311) -> Option<()> {
312 let token = token_from_offset(file, offset)?;
313 let parent = token.parent()?;
314
315 let name_node = if let Some(name) = ast::Name::cast(parent.clone()) {
316 name.syntax().clone()
317 } else if let Some(name_ref) = ast::NameRef::cast(parent) {
318 name_ref.syntax().clone()
319 } else {
320 return None;
321 };
322
323 let text = name_node.text().to_string();
324
325 if text.starts_with('"') {
326 return None;
327 }
328
329 let quoted = format!(r#""{}""#, text.to_lowercase());
330
331 actions.push(CodeAction {
332 title: "Quote identifier".to_owned(),
333 edits: vec![Edit::replace(name_node.text_range(), quoted)],
334 kind: ActionKind::RefactorRewrite,
335 });
336
337 Some(())
338}
339
340fn unquote_identifier(
341 actions: &mut Vec<CodeAction>,
342 file: &ast::SourceFile,
343 offset: TextSize,
344) -> Option<()> {
345 let token = token_from_offset(file, offset)?;
346 let parent = token.parent()?;
347
348 let name_node = if let Some(name) = ast::Name::cast(parent.clone()) {
349 name.syntax().clone()
350 } else if let Some(name_ref) = ast::NameRef::cast(parent) {
351 name_ref.syntax().clone()
352 } else {
353 return None;
354 };
355
356 let unquoted = unquote_ident(&name_node)?;
357
358 actions.push(CodeAction {
359 title: "Unquote identifier".to_owned(),
360 edits: vec![Edit::replace(name_node.text_range(), unquoted)],
361 kind: ActionKind::RefactorRewrite,
362 });
363
364 Some(())
365}
366
367fn add_explicit_alias(
371 actions: &mut Vec<CodeAction>,
372 file: &ast::SourceFile,
373 offset: TextSize,
374) -> Option<()> {
375 let token = token_from_offset(file, offset)?;
376 let target = token.parent_ancestors().find_map(ast::Target::cast)?;
377
378 if target.as_name().is_some() {
379 return None;
380 }
381
382 let alias = ColumnName::from_target(target.clone()).and_then(|c| c.0.to_string())?;
383
384 let expr_end = target.expr().map(|e| e.syntax().text_range().end())?;
385
386 let quoted_alias = quote_column_alias(&alias);
387 let replacement = format!(" as {}", quoted_alias);
390
391 actions.push(CodeAction {
392 title: "Add explicit alias".to_owned(),
393 edits: vec![Edit::insert(replacement, expr_end)],
394 kind: ActionKind::RefactorRewrite,
395 });
396
397 Some(())
398}
399
400fn remove_redundant_alias(
401 actions: &mut Vec<CodeAction>,
402 file: &ast::SourceFile,
403 offset: TextSize,
404) -> Option<()> {
405 let token = token_from_offset(file, offset)?;
406 let target = token.parent_ancestors().find_map(ast::Target::cast)?;
407
408 let as_name = target.as_name()?;
409 let (inferred_column, _) = ColumnName::inferred_from_target(target.clone())?;
410 let inferred_column_alias = inferred_column.to_string()?;
411
412 let alias = as_name.name()?;
413
414 if Name::from_node(&alias) != Name::from_string(inferred_column_alias) {
415 return None;
416 }
417
418 let expr_end = target.expr()?.syntax().text_range().end();
426 let alias_end = as_name.syntax().text_range().end();
427
428 actions.push(CodeAction {
429 title: "Remove redundant alias".to_owned(),
430 edits: vec![Edit::delete(TextRange::new(expr_end, alias_end))],
431 kind: ActionKind::QuickFix,
432 });
433
434 Some(())
435}
436
437#[cfg(test)]
438mod test {
439 use super::*;
440 use crate::test_utils::fixture;
441 use insta::assert_snapshot;
442 use rowan::TextSize;
443 use squawk_syntax::ast;
444
445 fn apply_code_action(
446 f: impl Fn(&mut Vec<CodeAction>, &ast::SourceFile, TextSize) -> Option<()>,
447 sql: &str,
448 ) -> String {
449 let (mut offset, sql) = fixture(sql);
450 let parse = ast::SourceFile::parse(&sql);
451 assert_eq!(parse.errors(), vec![]);
452 let file: ast::SourceFile = parse.tree();
453
454 offset = offset.checked_sub(1.into()).unwrap_or_default();
455
456 let mut actions = vec![];
457 f(&mut actions, &file, offset);
458
459 assert!(
460 !actions.is_empty(),
461 "We should always have actions for `apply_code_action`. If you want to ensure there are no actions, use `code_action_not_applicable` instead."
462 );
463
464 let action = &actions[0];
465 let mut result = sql.clone();
466
467 let mut edits = action.edits.clone();
468 edits.sort_by_key(|e| e.text_range.start());
469 check_overlap(&edits);
470 edits.reverse();
471
472 for edit in edits {
473 let start: usize = edit.text_range.start().into();
474 let end: usize = edit.text_range.end().into();
475 let replacement = edit.text.as_deref().unwrap_or("");
476 result.replace_range(start..end, replacement);
477 }
478
479 let reparse = ast::SourceFile::parse(&result);
480 assert_eq!(
481 reparse.errors(),
482 vec![],
483 "Code actions shouldn't cause syntax errors"
484 );
485
486 result
487 }
488
489 fn check_overlap(edits: &[Edit]) {
494 for (edit_i, edit_j) in edits.iter().zip(edits.iter().skip(1)) {
495 if let Some(intersection) = edit_i.text_range.intersect(edit_j.text_range) {
496 assert!(
497 intersection.is_empty(),
498 "Edit ranges must not overlap: {:?} and {:?} intersect at {:?}",
499 edit_i.text_range,
500 edit_j.text_range,
501 intersection
502 );
503 }
504 }
505 }
506
507 fn code_action_not_applicable(
508 f: impl Fn(&mut Vec<CodeAction>, &ast::SourceFile, TextSize) -> Option<()>,
509 sql: &str,
510 ) -> bool {
511 let (offset, sql) = fixture(sql);
512 let parse = ast::SourceFile::parse(&sql);
513 assert_eq!(parse.errors(), vec![]);
514 let file: ast::SourceFile = parse.tree();
515
516 let mut actions = vec![];
517 f(&mut actions, &file, offset);
518 actions.is_empty()
519 }
520
521 #[test]
522 fn remove_else_clause_() {
523 assert_snapshot!(apply_code_action(
524 remove_else_clause,
525 "select case x when true then 1 else$0 2 end;"),
526 @"select case x when true then 1 end;"
527 );
528 }
529
530 #[test]
531 fn remove_else_clause_before_token() {
532 assert_snapshot!(apply_code_action(
533 remove_else_clause,
534 "select case x when true then 1 e$0lse 2 end;"),
535 @"select case x when true then 1 end;"
536 );
537 }
538
539 #[test]
540 fn remove_else_clause_not_applicable() {
541 assert!(code_action_not_applicable(
542 remove_else_clause,
543 "select case x when true then 1 else 2 end$0;"
544 ));
545 }
546
547 #[test]
548 fn rewrite_string() {
549 assert_snapshot!(apply_code_action(
550 rewrite_as_dollar_quoted_string,
551 "select 'fo$0o';"),
552 @"select $$foo$$;"
553 );
554 }
555
556 #[test]
557 fn rewrite_string_with_single_quote() {
558 assert_snapshot!(apply_code_action(
559 rewrite_as_dollar_quoted_string,
560 "select 'it''s$0 nice';"),
561 @"select $$it's nice$$;"
562 );
563 }
564
565 #[test]
566 fn rewrite_string_with_dollar_signs() {
567 assert_snapshot!(apply_code_action(
568 rewrite_as_dollar_quoted_string,
569 "select 'foo $$ ba$0r';"),
570 @"select $q$foo $$ bar$q$;"
571 );
572 }
573
574 #[test]
575 fn rewrite_string_when_trailing_dollar() {
576 assert_snapshot!(apply_code_action(
577 rewrite_as_dollar_quoted_string,
578 "select 'foo $'$0;"),
579 @"select $q$foo $$q$;"
580 );
581 }
582
583 #[test]
584 fn rewrite_string_not_applicable() {
585 assert!(code_action_not_applicable(
586 rewrite_as_dollar_quoted_string,
587 "select 1 + $0 2;"
588 ));
589 }
590
591 #[test]
592 fn rewrite_prefix_string_not_applicable() {
593 assert!(code_action_not_applicable(
594 rewrite_as_dollar_quoted_string,
595 "select b'foo$0';"
596 ));
597 }
598
599 #[test]
600 fn rewrite_dollar_string() {
601 assert_snapshot!(apply_code_action(
602 rewrite_as_regular_string,
603 "select $$fo$0o$$;"),
604 @"select 'foo';"
605 );
606 }
607
608 #[test]
609 fn rewrite_dollar_string_with_tag() {
610 assert_snapshot!(apply_code_action(
611 rewrite_as_regular_string,
612 "select $tag$fo$0o$tag$;"),
613 @"select 'foo';"
614 );
615 }
616
617 #[test]
618 fn rewrite_dollar_string_with_quote() {
619 assert_snapshot!(apply_code_action(
620 rewrite_as_regular_string,
621 "select $$it'$0s fine$$;"),
622 @"select 'it''s fine';"
623 );
624 }
625
626 #[test]
627 fn rewrite_dollar_string_not_applicable() {
628 assert!(code_action_not_applicable(
629 rewrite_as_regular_string,
630 "select 'foo$0';"
631 ));
632 }
633
634 #[test]
635 fn rewrite_table_as_select_simple() {
636 assert_snapshot!(apply_code_action(
637 rewrite_table_as_select,
638 "tab$0le foo;"),
639 @"select * from foo;"
640 );
641 }
642
643 #[test]
644 fn rewrite_table_as_select_qualified() {
645 assert_snapshot!(apply_code_action(
646 rewrite_table_as_select,
647 "ta$0ble schema.foo;"),
648 @"select * from schema.foo;"
649 );
650 }
651
652 #[test]
653 fn rewrite_table_as_select_after_keyword() {
654 assert_snapshot!(apply_code_action(
655 rewrite_table_as_select,
656 "table$0 bar;"),
657 @"select * from bar;"
658 );
659 }
660
661 #[test]
662 fn rewrite_table_as_select_on_table_name() {
663 assert_snapshot!(apply_code_action(
664 rewrite_table_as_select,
665 "table fo$0o;"),
666 @"select * from foo;"
667 );
668 }
669
670 #[test]
671 fn rewrite_table_as_select_not_applicable() {
672 assert!(code_action_not_applicable(
673 rewrite_table_as_select,
674 "select * from foo$0;"
675 ));
676 }
677
678 #[test]
679 fn rewrite_select_as_table_simple() {
680 assert_snapshot!(apply_code_action(
681 rewrite_select_as_table,
682 "sel$0ect * from foo;"),
683 @"table foo;"
684 );
685 }
686
687 #[test]
688 fn rewrite_select_as_table_qualified() {
689 assert_snapshot!(apply_code_action(
690 rewrite_select_as_table,
691 "select * from sch$0ema.foo;"),
692 @"table schema.foo;"
693 );
694 }
695
696 #[test]
697 fn rewrite_select_as_table_on_star() {
698 assert_snapshot!(apply_code_action(
699 rewrite_select_as_table,
700 "select $0* from bar;"),
701 @"table bar;"
702 );
703 }
704
705 #[test]
706 fn rewrite_select_as_table_on_from() {
707 assert_snapshot!(apply_code_action(
708 rewrite_select_as_table,
709 "select * fr$0om baz;"),
710 @"table baz;"
711 );
712 }
713
714 #[test]
715 fn rewrite_select_as_table_not_applicable_with_where() {
716 assert!(code_action_not_applicable(
717 rewrite_select_as_table,
718 "select * from foo$0 where x = 1;"
719 ));
720 }
721
722 #[test]
723 fn rewrite_select_as_table_not_applicable_with_order_by() {
724 assert!(code_action_not_applicable(
725 rewrite_select_as_table,
726 "select * from foo$0 order by x;"
727 ));
728 }
729
730 #[test]
731 fn rewrite_select_as_table_not_applicable_with_limit() {
732 assert!(code_action_not_applicable(
733 rewrite_select_as_table,
734 "select * from foo$0 limit 10;"
735 ));
736 }
737
738 #[test]
739 fn rewrite_select_as_table_not_applicable_with_distinct() {
740 assert!(code_action_not_applicable(
741 rewrite_select_as_table,
742 "select distinct * from foo$0;"
743 ));
744 }
745
746 #[test]
747 fn rewrite_select_as_table_not_applicable_with_columns() {
748 assert!(code_action_not_applicable(
749 rewrite_select_as_table,
750 "select id, name from foo$0;"
751 ));
752 }
753
754 #[test]
755 fn rewrite_select_as_table_not_applicable_with_join() {
756 assert!(code_action_not_applicable(
757 rewrite_select_as_table,
758 "select * from foo$0 join bar on foo.id = bar.id;"
759 ));
760 }
761
762 #[test]
763 fn rewrite_select_as_table_not_applicable_with_alias() {
764 assert!(code_action_not_applicable(
765 rewrite_select_as_table,
766 "select * from foo$0 f;"
767 ));
768 }
769
770 #[test]
771 fn rewrite_select_as_table_not_applicable_with_multiple_tables() {
772 assert!(code_action_not_applicable(
773 rewrite_select_as_table,
774 "select * from foo$0, bar;"
775 ));
776 }
777
778 #[test]
779 fn rewrite_select_as_table_not_applicable_on_table() {
780 assert!(code_action_not_applicable(
781 rewrite_select_as_table,
782 "table foo$0;"
783 ));
784 }
785
786 #[test]
787 fn quote_identifier_on_name_ref() {
788 assert_snapshot!(apply_code_action(
789 quote_identifier,
790 "select x$0 from t;"),
791 @r#"select "x" from t;"#
792 );
793 }
794
795 #[test]
796 fn quote_identifier_on_name() {
797 assert_snapshot!(apply_code_action(
798 quote_identifier,
799 "create table T(X$0 int);"),
800 @r#"create table T("x" int);"#
801 );
802 }
803
804 #[test]
805 fn quote_identifier_lowercases() {
806 assert_snapshot!(apply_code_action(
807 quote_identifier,
808 "create table T(COL$0 int);"),
809 @r#"create table T("col" int);"#
810 );
811 }
812
813 #[test]
814 fn quote_identifier_not_applicable_when_already_quoted() {
815 assert!(code_action_not_applicable(
816 quote_identifier,
817 r#"select "x"$0 from t;"#
818 ));
819 }
820
821 #[test]
822 fn quote_identifier_not_applicable_on_select_keyword() {
823 assert!(code_action_not_applicable(
824 quote_identifier,
825 "sel$0ect x from t;"
826 ));
827 }
828
829 #[test]
830 fn quote_identifier_on_keyword_column_name() {
831 assert_snapshot!(apply_code_action(
832 quote_identifier,
833 "select te$0xt from t;"),
834 @r#"select "text" from t;"#
835 );
836 }
837
838 #[test]
839 fn quote_identifier_example_select() {
840 assert_snapshot!(apply_code_action(
841 quote_identifier,
842 "select x$0 from t;"),
843 @r#"select "x" from t;"#
844 );
845 }
846
847 #[test]
848 fn quote_identifier_example_create_table() {
849 assert_snapshot!(apply_code_action(
850 quote_identifier,
851 "create table T(X$0 int);"),
852 @r#"create table T("x" int);"#
853 );
854 }
855
856 #[test]
857 fn unquote_identifier_simple() {
858 assert_snapshot!(apply_code_action(
859 unquote_identifier,
860 r#"select "x"$0 from t;"#),
861 @"select x from t;"
862 );
863 }
864
865 #[test]
866 fn unquote_identifier_with_underscore() {
867 assert_snapshot!(apply_code_action(
868 unquote_identifier,
869 r#"select "user_id"$0 from t;"#),
870 @"select user_id from t;"
871 );
872 }
873
874 #[test]
875 fn unquote_identifier_with_digits() {
876 assert_snapshot!(apply_code_action(
877 unquote_identifier,
878 r#"select "x123"$0 from t;"#),
879 @"select x123 from t;"
880 );
881 }
882
883 #[test]
884 fn unquote_identifier_with_dollar() {
885 assert_snapshot!(apply_code_action(
886 unquote_identifier,
887 r#"select "my_table$1"$0 from t;"#),
888 @"select my_table$1 from t;"
889 );
890 }
891
892 #[test]
893 fn unquote_identifier_starts_with_underscore() {
894 assert_snapshot!(apply_code_action(
895 unquote_identifier,
896 r#"select "_col"$0 from t;"#),
897 @"select _col from t;"
898 );
899 }
900
901 #[test]
902 fn unquote_identifier_starts_with_unicode() {
903 assert_snapshot!(apply_code_action(
904 unquote_identifier,
905 r#"select "é"$0 from t;"#),
906 @"select é from t;"
907 );
908 }
909
910 #[test]
911 fn unquote_identifier_not_applicable() {
912 assert!(code_action_not_applicable(
914 unquote_identifier,
915 r#"select "X"$0 from t;"#
916 ));
917 assert!(code_action_not_applicable(
919 unquote_identifier,
920 r#"select "Foo"$0 from t;"#
921 ));
922 assert!(code_action_not_applicable(
924 unquote_identifier,
925 r#"select "my-col"$0 from t;"#
926 ));
927 assert!(code_action_not_applicable(
929 unquote_identifier,
930 r#"select "123"$0 from t;"#
931 ));
932 assert!(code_action_not_applicable(
934 unquote_identifier,
935 r#"select "foo bar"$0 from t;"#
936 ));
937 assert!(code_action_not_applicable(
939 unquote_identifier,
940 r#"select "foo""bar"$0 from t;"#
941 ));
942 assert!(code_action_not_applicable(
944 unquote_identifier,
945 "select x$0 from t;"
946 ));
947 assert!(code_action_not_applicable(
949 unquote_identifier,
950 r#"select "my[col]"$0 from t;"#
951 ));
952 assert!(code_action_not_applicable(
954 unquote_identifier,
955 r#"select "my{}"$0 from t;"#
956 ));
957 assert!(code_action_not_applicable(
959 unquote_identifier,
960 r#"select "select"$0 from t;"#
961 ));
962 }
963
964 #[test]
965 fn unquote_identifier_on_name() {
966 assert_snapshot!(apply_code_action(
967 unquote_identifier,
968 r#"create table T("x"$0 int);"#),
969 @"create table T(x int);"
970 );
971 }
972
973 #[test]
974 fn add_explicit_alias_simple_column() {
975 assert_snapshot!(apply_code_action(
976 add_explicit_alias,
977 "select col_na$0me from t;"),
978 @"select col_name as col_name from t;"
979 );
980 }
981
982 #[test]
983 fn add_explicit_alias_quoted_identifier() {
984 assert_snapshot!(apply_code_action(
985 add_explicit_alias,
986 r#"select "b"$0 from t;"#),
987 @r#"select "b" as b from t;"#
988 );
989 }
990
991 #[test]
992 fn add_explicit_alias_field_expr() {
993 assert_snapshot!(apply_code_action(
994 add_explicit_alias,
995 "select t.col$0umn from t;"),
996 @"select t.column as column from t;"
997 );
998 }
999
1000 #[test]
1001 fn add_explicit_alias_function_call() {
1002 assert_snapshot!(apply_code_action(
1003 add_explicit_alias,
1004 "select cou$0nt(*) from t;"),
1005 @"select count(*) as count from t;"
1006 );
1007 }
1008
1009 #[test]
1010 fn add_explicit_alias_cast_to_type() {
1011 assert_snapshot!(apply_code_action(
1012 add_explicit_alias,
1013 "select '1'::bigi$0nt from t;"),
1014 @"select '1'::bigint as int8 from t;"
1015 );
1016 }
1017
1018 #[test]
1019 fn add_explicit_alias_cast_column() {
1020 assert_snapshot!(apply_code_action(
1021 add_explicit_alias,
1022 "select col_na$0me::text from t;"),
1023 @"select col_name::text as col_name from t;"
1024 );
1025 }
1026
1027 #[test]
1028 fn add_explicit_alias_case_expr() {
1029 assert_snapshot!(apply_code_action(
1030 add_explicit_alias,
1031 "select ca$0se when true then 'a' end from t;"),
1032 @"select case when true then 'a' end as case from t;"
1033 );
1034 }
1035
1036 #[test]
1037 fn add_explicit_alias_case_with_else() {
1038 assert_snapshot!(apply_code_action(
1039 add_explicit_alias,
1040 "select ca$0se when true then 'a' else now()::text end from t;"),
1041 @"select case when true then 'a' else now()::text end as now from t;"
1042 );
1043 }
1044
1045 #[test]
1046 fn add_explicit_alias_array() {
1047 assert_snapshot!(apply_code_action(
1048 add_explicit_alias,
1049 "select arr$0ay[1, 2, 3] from t;"),
1050 @"select array[1, 2, 3] as array from t;"
1051 );
1052 }
1053
1054 #[test]
1055 fn add_explicit_alias_not_applicable_already_has_alias() {
1056 assert!(code_action_not_applicable(
1057 add_explicit_alias,
1058 "select col_name$0 as foo from t;"
1059 ));
1060 }
1061
1062 #[test]
1063 fn add_explicit_alias_unknown_column() {
1064 assert_snapshot!(apply_code_action(
1065 add_explicit_alias,
1066 "select 1 $0+ 2 from t;"),
1067 @r#"select 1 + 2 as "?column?" from t;"#
1068 );
1069 }
1070
1071 #[test]
1072 fn add_explicit_alias_not_applicable_star() {
1073 assert!(code_action_not_applicable(
1074 add_explicit_alias,
1075 "select $0* from t;"
1076 ));
1077 }
1078
1079 #[test]
1080 fn add_explicit_alias_literal() {
1081 assert_snapshot!(apply_code_action(
1082 add_explicit_alias,
1083 "select 'foo$0' from t;"),
1084 @r#"select 'foo' as "?column?" from t;"#
1085 );
1086 }
1087
1088 #[test]
1089 fn remove_redundant_alias_simple() {
1090 assert_snapshot!(apply_code_action(
1091 remove_redundant_alias,
1092 "select col_name as col_na$0me from t;"),
1093 @"select col_name from t;"
1094 );
1095 }
1096
1097 #[test]
1098 fn remove_redundant_alias_quoted() {
1099 assert_snapshot!(apply_code_action(
1100 remove_redundant_alias,
1101 r#"select "x"$0 as x from t;"#),
1102 @r#"select "x" from t;"#
1103 );
1104 }
1105
1106 #[test]
1107 fn remove_redundant_alias_case_insensitive() {
1108 assert_snapshot!(apply_code_action(
1109 remove_redundant_alias,
1110 "select col_name$0 as COL_NAME from t;"),
1111 @"select col_name from t;"
1112 );
1113 }
1114
1115 #[test]
1116 fn remove_redundant_alias_function() {
1117 assert_snapshot!(apply_code_action(
1118 remove_redundant_alias,
1119 "select count(*)$0 as count from t;"),
1120 @"select count(*) from t;"
1121 );
1122 }
1123
1124 #[test]
1125 fn remove_redundant_alias_field_expr() {
1126 assert_snapshot!(apply_code_action(
1127 remove_redundant_alias,
1128 "select t.col$0umn as column from t;"),
1129 @"select t.column from t;"
1130 );
1131 }
1132
1133 #[test]
1134 fn remove_redundant_alias_not_applicable_different_name() {
1135 assert!(code_action_not_applicable(
1136 remove_redundant_alias,
1137 "select col_name$0 as foo from t;"
1138 ));
1139 }
1140
1141 #[test]
1142 fn remove_redundant_alias_not_applicable_no_alias() {
1143 assert!(code_action_not_applicable(
1144 remove_redundant_alias,
1145 "select col_name$0 from t;"
1146 ));
1147 }
1148}