1use itertools::Itertools;
2use rowan::{TextRange, TextSize};
3use squawk_linter::Edit;
4use squawk_syntax::{
5 SyntaxKind, SyntaxToken,
6 ast::{self, AstNode},
7};
8use std::iter;
9
10use crate::{
11 binder,
12 column_name::ColumnName,
13 offsets::token_from_offset,
14 quote::{quote_column_alias, unquote_ident},
15 symbols::Name,
16};
17
18#[derive(Debug, Clone)]
19pub enum ActionKind {
20 QuickFix,
21 RefactorRewrite,
22}
23
24#[derive(Debug, Clone)]
25pub struct CodeAction {
26 pub title: String,
27 pub edits: Vec<Edit>,
28 pub kind: ActionKind,
29}
30
31pub fn code_actions(file: ast::SourceFile, offset: TextSize) -> Option<Vec<CodeAction>> {
32 let mut actions = vec![];
33 rewrite_as_regular_string(&mut actions, &file, offset);
34 rewrite_as_dollar_quoted_string(&mut actions, &file, offset);
35 remove_else_clause(&mut actions, &file, offset);
36 rewrite_table_as_select(&mut actions, &file, offset);
37 rewrite_select_as_table(&mut actions, &file, offset);
38 rewrite_values_as_select(&mut actions, &file, offset);
39 rewrite_select_as_values(&mut actions, &file, offset);
40 add_schema(&mut actions, &file, offset);
41 quote_identifier(&mut actions, &file, offset);
42 unquote_identifier(&mut actions, &file, offset);
43 add_explicit_alias(&mut actions, &file, offset);
44 remove_redundant_alias(&mut actions, &file, offset);
45 rewrite_cast_to_double_colon(&mut actions, &file, offset);
46 rewrite_double_colon_to_cast(&mut actions, &file, offset);
47 Some(actions)
48}
49
50fn rewrite_as_regular_string(
51 actions: &mut Vec<CodeAction>,
52 file: &ast::SourceFile,
53 offset: TextSize,
54) -> Option<()> {
55 let dollar_string = file
56 .syntax()
57 .token_at_offset(offset)
58 .find(|token| token.kind() == SyntaxKind::DOLLAR_QUOTED_STRING)?;
59
60 let replacement = dollar_quoted_to_string(dollar_string.text())?;
61 actions.push(CodeAction {
62 title: "Rewrite as regular string".to_owned(),
63 edits: vec![Edit::replace(dollar_string.text_range(), replacement)],
64 kind: ActionKind::RefactorRewrite,
65 });
66
67 Some(())
68}
69
70fn rewrite_as_dollar_quoted_string(
71 actions: &mut Vec<CodeAction>,
72 file: &ast::SourceFile,
73 offset: TextSize,
74) -> Option<()> {
75 let string = file
76 .syntax()
77 .token_at_offset(offset)
78 .find(|token| token.kind() == SyntaxKind::STRING)?;
79
80 let replacement = string_to_dollar_quoted(string.text())?;
81 actions.push(CodeAction {
82 title: "Rewrite as dollar-quoted string".to_owned(),
83 edits: vec![Edit::replace(string.text_range(), replacement)],
84 kind: ActionKind::RefactorRewrite,
85 });
86
87 Some(())
88}
89
90fn string_to_dollar_quoted(text: &str) -> Option<String> {
91 let normalized = normalize_single_quoted_string(text)?;
92 let delimiter = dollar_delimiter(&normalized)?;
93 let boundary = format!("${}$", delimiter);
94 Some(format!("{boundary}{normalized}{boundary}"))
95}
96
97fn dollar_quoted_to_string(text: &str) -> Option<String> {
98 debug_assert!(text.starts_with('$'));
99 let (delimiter, content) = split_dollar_quoted(text)?;
100 let boundary = format!("${}$", delimiter);
101
102 if !text.starts_with(&boundary) || !text.ends_with(&boundary) {
103 return None;
104 }
105
106 let escaped = content.replace('\'', "''");
108 Some(format!("'{}'", escaped))
109}
110
111fn split_dollar_quoted(text: &str) -> Option<(String, &str)> {
112 debug_assert!(text.starts_with('$'));
113 let second_dollar = text[1..].find('$')?;
114 let delimiter = &text[1..=second_dollar];
116 let boundary = format!("${}$", delimiter);
117
118 if !text.ends_with(&boundary) {
119 return None;
120 }
121
122 let start = boundary.len();
123 let end = text.len().checked_sub(boundary.len())?;
124 let content = text.get(start..end)?;
125 Some((delimiter.to_owned(), content))
126}
127
128fn normalize_single_quoted_string(text: &str) -> Option<String> {
129 let body = text.strip_prefix('\'')?.strip_suffix('\'')?;
130 return Some(body.replace("''", "'"));
131}
132
133fn dollar_delimiter(content: &str) -> Option<String> {
134 if !content.contains("$$") && !content.ends_with('$') {
137 return Some("".to_owned());
138 }
139
140 let mut delim = "q".to_owned();
141 for idx in 0..10 {
143 if !content.contains(&format!("${}$", delim)) {
144 return Some(delim);
145 }
146 delim.push_str(&idx.to_string());
147 }
148 None
149}
150
151fn remove_else_clause(
152 actions: &mut Vec<CodeAction>,
153 file: &ast::SourceFile,
154 offset: TextSize,
155) -> Option<()> {
156 let else_token = file
157 .syntax()
158 .token_at_offset(offset)
159 .find(|x| x.kind() == SyntaxKind::ELSE_KW)?;
160 let parent = else_token.parent()?;
161 let else_clause = ast::ElseClause::cast(parent)?;
162
163 let mut edits = vec![];
164 edits.push(Edit::delete(else_clause.syntax().text_range()));
165 if let Some(token) = else_token.prev_token() {
166 if token.kind() == SyntaxKind::WHITESPACE {
167 edits.push(Edit::delete(token.text_range()));
168 }
169 }
170
171 actions.push(CodeAction {
172 title: "Remove `else` clause".to_owned(),
173 edits,
174 kind: ActionKind::RefactorRewrite,
175 });
176 Some(())
177}
178
179fn rewrite_table_as_select(
180 actions: &mut Vec<CodeAction>,
181 file: &ast::SourceFile,
182 offset: TextSize,
183) -> Option<()> {
184 let token = token_from_offset(file, offset)?;
185 let table = token.parent_ancestors().find_map(ast::Table::cast)?;
186
187 let relation_name = table.relation_name()?;
188 let table_name = relation_name.syntax().text();
189
190 let replacement = format!("select * from {}", table_name);
191
192 actions.push(CodeAction {
193 title: "Rewrite as `select`".to_owned(),
194 edits: vec![Edit::replace(table.syntax().text_range(), replacement)],
195 kind: ActionKind::RefactorRewrite,
196 });
197
198 Some(())
199}
200
201fn rewrite_select_as_table(
202 actions: &mut Vec<CodeAction>,
203 file: &ast::SourceFile,
204 offset: TextSize,
205) -> Option<()> {
206 let token = token_from_offset(file, offset)?;
207 let select = token.parent_ancestors().find_map(ast::Select::cast)?;
208
209 if !can_transform_select_to_table(&select) {
210 return None;
211 }
212
213 let from_clause = select.from_clause()?;
214 let from_item = from_clause.from_items().next()?;
215
216 let table_name = if let Some(name_ref) = from_item.name_ref() {
217 name_ref.syntax().text().to_string()
218 } else if let Some(field_expr) = from_item.field_expr() {
219 field_expr.syntax().text().to_string()
220 } else {
221 return None;
222 };
223
224 let replacement = format!("table {}", table_name);
225
226 actions.push(CodeAction {
227 title: "Rewrite as `table`".to_owned(),
228 edits: vec![Edit::replace(select.syntax().text_range(), replacement)],
229 kind: ActionKind::RefactorRewrite,
230 });
231
232 Some(())
233}
234
235fn can_transform_select_to_table(select: &ast::Select) -> bool {
242 if select.with_clause().is_some()
243 || select.where_clause().is_some()
244 || select.group_by_clause().is_some()
245 || select.having_clause().is_some()
246 || select.window_clause().is_some()
247 || select.order_by_clause().is_some()
248 || select.limit_clause().is_some()
249 || select.fetch_clause().is_some()
250 || select.offset_clause().is_some()
251 || select.filter_clause().is_some()
252 || select.locking_clauses().next().is_some()
253 {
254 return false;
255 }
256
257 let Some(select_clause) = select.select_clause() else {
258 return false;
259 };
260
261 if select_clause.distinct_clause().is_some() {
262 return false;
263 }
264
265 let Some(target_list) = select_clause.target_list() else {
266 return false;
267 };
268
269 let mut targets = target_list.targets();
270 let Some(target) = targets.next() else {
271 return false;
272 };
273
274 if targets.next().is_some() {
275 return false;
276 }
277
278 if target.expr().is_some() || target.star_token().is_none() {
280 return false;
281 }
282
283 let Some(from_clause) = select.from_clause() else {
284 return false;
285 };
286
287 let mut from_items = from_clause.from_items();
288 let Some(from_item) = from_items.next() else {
289 return false;
290 };
291
292 if from_items.next().is_some() || from_clause.join_exprs().next().is_some() {
294 return false;
295 }
296
297 if from_item.alias().is_some()
298 || from_item.tablesample_clause().is_some()
299 || from_item.only_token().is_some()
300 || from_item.lateral_token().is_some()
301 || from_item.star_token().is_some()
302 || from_item.call_expr().is_some()
303 || from_item.paren_select().is_some()
304 || from_item.json_table().is_some()
305 || from_item.xml_table().is_some()
306 || from_item.cast_expr().is_some()
307 {
308 return false;
309 }
310
311 from_item.name_ref().is_some() || from_item.field_expr().is_some()
313}
314
315fn quote_identifier(
316 actions: &mut Vec<CodeAction>,
317 file: &ast::SourceFile,
318 offset: TextSize,
319) -> Option<()> {
320 let token = token_from_offset(file, offset)?;
321 let parent = token.parent()?;
322
323 let name_node = if let Some(name) = ast::Name::cast(parent.clone()) {
324 name.syntax().clone()
325 } else if let Some(name_ref) = ast::NameRef::cast(parent) {
326 name_ref.syntax().clone()
327 } else {
328 return None;
329 };
330
331 let text = name_node.text().to_string();
332
333 if text.starts_with('"') {
334 return None;
335 }
336
337 let quoted = format!(r#""{}""#, text.to_lowercase());
338
339 actions.push(CodeAction {
340 title: "Quote identifier".to_owned(),
341 edits: vec![Edit::replace(name_node.text_range(), quoted)],
342 kind: ActionKind::RefactorRewrite,
343 });
344
345 Some(())
346}
347
348fn unquote_identifier(
349 actions: &mut Vec<CodeAction>,
350 file: &ast::SourceFile,
351 offset: TextSize,
352) -> Option<()> {
353 let token = token_from_offset(file, offset)?;
354 let parent = token.parent()?;
355
356 let name_node = if let Some(name) = ast::Name::cast(parent.clone()) {
357 name.syntax().clone()
358 } else if let Some(name_ref) = ast::NameRef::cast(parent) {
359 name_ref.syntax().clone()
360 } else {
361 return None;
362 };
363
364 let unquoted = unquote_ident(&name_node)?;
365
366 actions.push(CodeAction {
367 title: "Unquote identifier".to_owned(),
368 edits: vec![Edit::replace(name_node.text_range(), unquoted)],
369 kind: ActionKind::RefactorRewrite,
370 });
371
372 Some(())
373}
374
375fn add_explicit_alias(
379 actions: &mut Vec<CodeAction>,
380 file: &ast::SourceFile,
381 offset: TextSize,
382) -> Option<()> {
383 let token = token_from_offset(file, offset)?;
384 let target = token.parent_ancestors().find_map(ast::Target::cast)?;
385
386 if target.as_name().is_some() {
387 return None;
388 }
389
390 let alias = ColumnName::from_target(target.clone()).and_then(|c| c.0.to_string())?;
391
392 let expr_end = target.expr().map(|e| e.syntax().text_range().end())?;
393
394 let quoted_alias = quote_column_alias(&alias);
395 let replacement = format!(" as {}", quoted_alias);
398
399 actions.push(CodeAction {
400 title: "Add explicit alias".to_owned(),
401 edits: vec![Edit::insert(replacement, expr_end)],
402 kind: ActionKind::RefactorRewrite,
403 });
404
405 Some(())
406}
407
408fn remove_redundant_alias(
409 actions: &mut Vec<CodeAction>,
410 file: &ast::SourceFile,
411 offset: TextSize,
412) -> Option<()> {
413 let token = token_from_offset(file, offset)?;
414 let target = token.parent_ancestors().find_map(ast::Target::cast)?;
415
416 let as_name = target.as_name()?;
417 let (inferred_column, _) = ColumnName::inferred_from_target(target.clone())?;
418 let inferred_column_alias = inferred_column.to_string()?;
419
420 let alias = as_name.name()?;
421
422 if Name::from_node(&alias) != Name::from_string(inferred_column_alias) {
423 return None;
424 }
425
426 let expr_end = target.expr()?.syntax().text_range().end();
434 let alias_end = as_name.syntax().text_range().end();
435
436 actions.push(CodeAction {
437 title: "Remove redundant alias".to_owned(),
438 edits: vec![Edit::delete(TextRange::new(expr_end, alias_end))],
439 kind: ActionKind::QuickFix,
440 });
441
442 Some(())
443}
444
445fn add_schema(
446 actions: &mut Vec<CodeAction>,
447 file: &ast::SourceFile,
448 offset: TextSize,
449) -> Option<()> {
450 let token = token_from_offset(file, offset)?;
451 let range = token.parent_ancestors().find_map(|node| {
452 if let Some(path) = ast::Path::cast(node.clone()) {
453 if path.qualifier().is_some() {
454 return None;
455 }
456 return Some(path.syntax().text_range());
457 }
458 if let Some(from_item) = ast::FromItem::cast(node.clone()) {
459 let name_ref = from_item.name_ref()?;
460 return Some(name_ref.syntax().text_range());
461 }
462 if let Some(call_expr) = ast::CallExpr::cast(node) {
463 let ast::Expr::NameRef(name_ref) = call_expr.expr()? else {
464 return None;
465 };
466 return Some(name_ref.syntax().text_range());
467 }
468 None
469 })?;
470
471 if !range.contains(offset) {
472 return None;
473 }
474
475 let position = token.text_range().start();
476 let binder = binder::bind(file);
477 let schema = binder.search_path_at(position).first()?.to_string();
478 let replacement = format!("{}.", schema);
479
480 actions.push(CodeAction {
481 title: "Add schema".to_owned(),
482 edits: vec![Edit::insert(replacement, position)],
483 kind: ActionKind::RefactorRewrite,
484 });
485
486 Some(())
487}
488
489fn rewrite_cast_to_double_colon(
490 actions: &mut Vec<CodeAction>,
491 file: &ast::SourceFile,
492 offset: TextSize,
493) -> Option<()> {
494 let token = token_from_offset(file, offset)?;
495 let cast_expr = token.parent_ancestors().find_map(ast::CastExpr::cast)?;
496
497 if cast_expr.colon_colon().is_some() {
498 return None;
499 }
500
501 let expr = cast_expr.expr()?;
502 let ty = cast_expr.ty()?;
503
504 let expr_text = expr.syntax().text();
505 let type_text = ty.syntax().text();
506
507 let replacement = format!("{}::{}", expr_text, type_text);
508
509 actions.push(CodeAction {
510 title: "Rewrite as cast operator `::`".to_owned(),
511 edits: vec![Edit::replace(cast_expr.syntax().text_range(), replacement)],
512 kind: ActionKind::RefactorRewrite,
513 });
514
515 Some(())
516}
517
518fn rewrite_double_colon_to_cast(
519 actions: &mut Vec<CodeAction>,
520 file: &ast::SourceFile,
521 offset: TextSize,
522) -> Option<()> {
523 let token = token_from_offset(file, offset)?;
524 let cast_expr = token.parent_ancestors().find_map(ast::CastExpr::cast)?;
525
526 cast_expr.colon_colon()?;
527
528 let expr = cast_expr.expr()?;
529 let ty = cast_expr.ty()?;
530
531 let expr_text = expr.syntax().text();
532 let type_text = ty.syntax().text();
533
534 let replacement = format!("cast({} as {})", expr_text, type_text);
535
536 actions.push(CodeAction {
537 title: "Rewrite as cast function `cast()`".to_owned(),
538 edits: vec![Edit::replace(cast_expr.syntax().text_range(), replacement)],
539 kind: ActionKind::RefactorRewrite,
540 });
541
542 Some(())
543}
544
545fn rewrite_values_as_select(
546 actions: &mut Vec<CodeAction>,
547 file: &ast::SourceFile,
548 offset: TextSize,
549) -> Option<()> {
550 let token = token_from_offset(file, offset)?;
551 let values = token.parent_ancestors().find_map(ast::Values::cast)?;
552
553 let value_token_start = values.values_token().map(|x| x.text_range().start())?;
554 let values_end = values.syntax().text_range().end();
555 let values_range = TextRange::new(value_token_start, values_end);
557
558 let mut rows = values.row_list()?.rows();
559
560 let first_targets: Vec<_> = rows
561 .next()?
562 .exprs()
563 .enumerate()
564 .map(|(idx, expr)| format!("{} as column{}", expr.syntax().text(), idx + 1))
565 .collect();
566
567 if first_targets.is_empty() {
568 return None;
569 }
570
571 let mut select_parts = vec![format!("select {}", first_targets.join(", "))];
572
573 for row in rows {
574 let row_targets = row
575 .exprs()
576 .map(|e| e.syntax().text().to_string())
577 .join(", ");
578 if row_targets.is_empty() {
579 return None;
580 }
581 select_parts.push(format!("union all\nselect {}", row_targets));
582 }
583
584 let select_stmt = select_parts.join("\n");
585
586 actions.push(CodeAction {
587 title: "Rewrite as `select`".to_owned(),
588 edits: vec![Edit::replace(values_range, select_stmt)],
589 kind: ActionKind::RefactorRewrite,
590 });
591
592 Some(())
593}
594
595fn is_values_row_column_name(target: &ast::Target, idx: usize) -> bool {
596 let Some(as_name) = target.as_name() else {
597 return false;
598 };
599 let Some(name) = as_name.name() else {
600 return false;
601 };
602 let expected = format!("column{}", idx + 1);
603 if Name::from_node(&name) != Name::from_string(expected) {
604 return false;
605 }
606 true
607}
608
609enum SelectContext {
610 Compound(ast::CompoundSelect),
611 Single(ast::Select),
612}
613
614impl SelectContext {
615 fn iter(&self) -> Option<Box<dyn Iterator<Item = ast::Select>>> {
616 fn variant_iter(
620 variant: ast::SelectVariant,
621 ) -> Option<Box<dyn Iterator<Item = ast::Select>>> {
622 match variant {
623 ast::SelectVariant::Select(select) => Some(Box::new(iter::once(select))),
624 ast::SelectVariant::CompoundSelect(compound) => compound_iter(&compound),
625 ast::SelectVariant::ParenSelect(_)
626 | ast::SelectVariant::SelectInto(_)
627 | ast::SelectVariant::Table(_)
628 | ast::SelectVariant::Values(_) => None,
629 }
630 }
631
632 fn compound_iter(
633 node: &ast::CompoundSelect,
634 ) -> Option<Box<dyn Iterator<Item = ast::Select>>> {
635 let lhs_iter = node
636 .lhs()
637 .map(variant_iter)
638 .unwrap_or_else(|| Some(Box::new(iter::empty())))?;
639 let rhs_iter = node
640 .rhs()
641 .map(variant_iter)
642 .unwrap_or_else(|| Some(Box::new(iter::empty())))?;
643 Some(Box::new(lhs_iter.chain(rhs_iter)))
644 }
645
646 match self {
647 SelectContext::Compound(compound) => compound_iter(compound),
648 SelectContext::Single(select) => Some(Box::new(iter::once(select.clone()))),
649 }
650 }
651}
652
653fn rewrite_select_as_values(
654 actions: &mut Vec<CodeAction>,
655 file: &ast::SourceFile,
656 offset: TextSize,
657) -> Option<()> {
658 let token = token_from_offset(file, offset)?;
659
660 let parent = find_select_parent(token)?;
661
662 let mut selects = parent.iter()?.peekable();
663 let select_token_start = selects
664 .peek()?
665 .select_clause()
666 .and_then(|x| x.select_token())
667 .map(|x| x.text_range().start())?;
668
669 let mut rows = vec![];
670 for (idx, select) in selects.enumerate() {
671 let exprs: Vec<String> = select
672 .select_clause()?
673 .target_list()?
674 .targets()
675 .enumerate()
676 .map(|(i, t)| {
677 if idx != 0 || is_values_row_column_name(&t, i) {
678 t.expr().map(|expr| expr.syntax().text().to_string())
679 } else {
680 None
681 }
682 })
683 .collect::<Option<_>>()?;
684
685 if exprs.is_empty() {
686 return None;
687 }
688
689 rows.push(format!("({})", exprs.join(", ")));
690 }
691
692 let values_stmt = format!("values {}", rows.join(", "));
693
694 let select_end = match &parent {
695 SelectContext::Compound(compound) => compound.syntax().text_range().end(),
696 SelectContext::Single(select) => select.syntax().text_range().end(),
697 };
698 let select_range = TextRange::new(select_token_start, select_end);
699
700 actions.push(CodeAction {
701 title: "Rewrite as `values`".to_owned(),
702 edits: vec![Edit::replace(select_range, values_stmt)],
703 kind: ActionKind::RefactorRewrite,
704 });
705
706 Some(())
707}
708
709fn find_select_parent(token: SyntaxToken) -> Option<SelectContext> {
710 let mut found_select = None;
711 let mut found_compound = None;
712 for node in token.parent_ancestors() {
713 if let Some(compound_select) = ast::CompoundSelect::cast(node.clone()) {
714 if compound_select.union_token().is_some() && compound_select.all_token().is_some() {
715 found_compound = Some(SelectContext::Compound(compound_select));
716 } else {
717 break;
718 }
719 }
720 if found_select.is_none()
721 && let Some(select) = ast::Select::cast(node)
722 {
723 found_select = Some(SelectContext::Single(select));
724 }
725 }
726 found_compound.or(found_select)
727}
728
729#[cfg(test)]
730mod test {
731 use super::*;
732 use crate::test_utils::fixture;
733 use insta::assert_snapshot;
734 use rowan::TextSize;
735 use squawk_syntax::ast;
736
737 fn apply_code_action(
738 f: impl Fn(&mut Vec<CodeAction>, &ast::SourceFile, TextSize) -> Option<()>,
739 sql: &str,
740 ) -> String {
741 let (mut offset, sql) = fixture(sql);
742 let parse = ast::SourceFile::parse(&sql);
743 assert_eq!(parse.errors(), vec![]);
744 let file: ast::SourceFile = parse.tree();
745
746 offset = offset.checked_sub(1.into()).unwrap_or_default();
747
748 let mut actions = vec![];
749 f(&mut actions, &file, offset);
750
751 assert!(
752 !actions.is_empty(),
753 "We should always have actions for `apply_code_action`. If you want to ensure there are no actions, use `code_action_not_applicable` instead."
754 );
755
756 let action = &actions[0];
757 let mut result = sql.clone();
758
759 let mut edits = action.edits.clone();
760 edits.sort_by_key(|e| e.text_range.start());
761 check_overlap(&edits);
762 edits.reverse();
763
764 for edit in edits {
765 let start: usize = edit.text_range.start().into();
766 let end: usize = edit.text_range.end().into();
767 let replacement = edit.text.as_deref().unwrap_or("");
768 result.replace_range(start..end, replacement);
769 }
770
771 let reparse = ast::SourceFile::parse(&result);
772 assert_eq!(
773 reparse.errors(),
774 vec![],
775 "Code actions shouldn't cause syntax errors"
776 );
777
778 result
779 }
780
781 fn check_overlap(edits: &[Edit]) {
786 for (edit_i, edit_j) in edits.iter().zip(edits.iter().skip(1)) {
787 if let Some(intersection) = edit_i.text_range.intersect(edit_j.text_range) {
788 assert!(
789 intersection.is_empty(),
790 "Edit ranges must not overlap: {:?} and {:?} intersect at {:?}",
791 edit_i.text_range,
792 edit_j.text_range,
793 intersection
794 );
795 }
796 }
797 }
798
799 fn code_action_not_applicable(
800 f: impl Fn(&mut Vec<CodeAction>, &ast::SourceFile, TextSize) -> Option<()>,
801 sql: &str,
802 ) -> bool {
803 let (offset, sql) = fixture(sql);
804 let parse = ast::SourceFile::parse(&sql);
805 assert_eq!(parse.errors(), vec![]);
806 let file: ast::SourceFile = parse.tree();
807
808 let mut actions = vec![];
809 f(&mut actions, &file, offset);
810 actions.is_empty()
811 }
812
813 #[test]
814 fn remove_else_clause_() {
815 assert_snapshot!(apply_code_action(
816 remove_else_clause,
817 "select case x when true then 1 else$0 2 end;"),
818 @"select case x when true then 1 end;"
819 );
820 }
821
822 #[test]
823 fn remove_else_clause_before_token() {
824 assert_snapshot!(apply_code_action(
825 remove_else_clause,
826 "select case x when true then 1 e$0lse 2 end;"),
827 @"select case x when true then 1 end;"
828 );
829 }
830
831 #[test]
832 fn remove_else_clause_not_applicable() {
833 assert!(code_action_not_applicable(
834 remove_else_clause,
835 "select case x when true then 1 else 2 end$0;"
836 ));
837 }
838
839 #[test]
840 fn rewrite_string() {
841 assert_snapshot!(apply_code_action(
842 rewrite_as_dollar_quoted_string,
843 "select 'fo$0o';"),
844 @"select $$foo$$;"
845 );
846 }
847
848 #[test]
849 fn rewrite_string_with_single_quote() {
850 assert_snapshot!(apply_code_action(
851 rewrite_as_dollar_quoted_string,
852 "select 'it''s$0 nice';"),
853 @"select $$it's nice$$;"
854 );
855 }
856
857 #[test]
858 fn rewrite_string_with_dollar_signs() {
859 assert_snapshot!(apply_code_action(
860 rewrite_as_dollar_quoted_string,
861 "select 'foo $$ ba$0r';"),
862 @"select $q$foo $$ bar$q$;"
863 );
864 }
865
866 #[test]
867 fn rewrite_string_when_trailing_dollar() {
868 assert_snapshot!(apply_code_action(
869 rewrite_as_dollar_quoted_string,
870 "select 'foo $'$0;"),
871 @"select $q$foo $$q$;"
872 );
873 }
874
875 #[test]
876 fn rewrite_string_not_applicable() {
877 assert!(code_action_not_applicable(
878 rewrite_as_dollar_quoted_string,
879 "select 1 + $0 2;"
880 ));
881 }
882
883 #[test]
884 fn rewrite_prefix_string_not_applicable() {
885 assert!(code_action_not_applicable(
886 rewrite_as_dollar_quoted_string,
887 "select b'foo$0';"
888 ));
889 }
890
891 #[test]
892 fn rewrite_dollar_string() {
893 assert_snapshot!(apply_code_action(
894 rewrite_as_regular_string,
895 "select $$fo$0o$$;"),
896 @"select 'foo';"
897 );
898 }
899
900 #[test]
901 fn rewrite_dollar_string_with_tag() {
902 assert_snapshot!(apply_code_action(
903 rewrite_as_regular_string,
904 "select $tag$fo$0o$tag$;"),
905 @"select 'foo';"
906 );
907 }
908
909 #[test]
910 fn rewrite_dollar_string_with_quote() {
911 assert_snapshot!(apply_code_action(
912 rewrite_as_regular_string,
913 "select $$it'$0s fine$$;"),
914 @"select 'it''s fine';"
915 );
916 }
917
918 #[test]
919 fn rewrite_dollar_string_not_applicable() {
920 assert!(code_action_not_applicable(
921 rewrite_as_regular_string,
922 "select 'foo$0';"
923 ));
924 }
925
926 #[test]
927 fn rewrite_table_as_select_simple() {
928 assert_snapshot!(apply_code_action(
929 rewrite_table_as_select,
930 "tab$0le foo;"),
931 @"select * from foo;"
932 );
933 }
934
935 #[test]
936 fn rewrite_table_as_select_qualified() {
937 assert_snapshot!(apply_code_action(
938 rewrite_table_as_select,
939 "ta$0ble schema.foo;"),
940 @"select * from schema.foo;"
941 );
942 }
943
944 #[test]
945 fn rewrite_table_as_select_after_keyword() {
946 assert_snapshot!(apply_code_action(
947 rewrite_table_as_select,
948 "table$0 bar;"),
949 @"select * from bar;"
950 );
951 }
952
953 #[test]
954 fn rewrite_table_as_select_on_table_name() {
955 assert_snapshot!(apply_code_action(
956 rewrite_table_as_select,
957 "table fo$0o;"),
958 @"select * from foo;"
959 );
960 }
961
962 #[test]
963 fn rewrite_table_as_select_not_applicable() {
964 assert!(code_action_not_applicable(
965 rewrite_table_as_select,
966 "select * from foo$0;"
967 ));
968 }
969
970 #[test]
971 fn rewrite_select_as_table_simple() {
972 assert_snapshot!(apply_code_action(
973 rewrite_select_as_table,
974 "sel$0ect * from foo;"),
975 @"table foo;"
976 );
977 }
978
979 #[test]
980 fn rewrite_select_as_table_qualified() {
981 assert_snapshot!(apply_code_action(
982 rewrite_select_as_table,
983 "select * from sch$0ema.foo;"),
984 @"table schema.foo;"
985 );
986 }
987
988 #[test]
989 fn rewrite_select_as_table_on_star() {
990 assert_snapshot!(apply_code_action(
991 rewrite_select_as_table,
992 "select $0* from bar;"),
993 @"table bar;"
994 );
995 }
996
997 #[test]
998 fn rewrite_select_as_table_on_from() {
999 assert_snapshot!(apply_code_action(
1000 rewrite_select_as_table,
1001 "select * fr$0om baz;"),
1002 @"table baz;"
1003 );
1004 }
1005
1006 #[test]
1007 fn rewrite_select_as_table_not_applicable_with_where() {
1008 assert!(code_action_not_applicable(
1009 rewrite_select_as_table,
1010 "select * from foo$0 where x = 1;"
1011 ));
1012 }
1013
1014 #[test]
1015 fn rewrite_select_as_table_not_applicable_with_order_by() {
1016 assert!(code_action_not_applicable(
1017 rewrite_select_as_table,
1018 "select * from foo$0 order by x;"
1019 ));
1020 }
1021
1022 #[test]
1023 fn rewrite_select_as_table_not_applicable_with_limit() {
1024 assert!(code_action_not_applicable(
1025 rewrite_select_as_table,
1026 "select * from foo$0 limit 10;"
1027 ));
1028 }
1029
1030 #[test]
1031 fn add_schema_simple() {
1032 assert_snapshot!(apply_code_action(
1033 add_schema,
1034 "create table t$0(a text, b int);"),
1035 @"create table public.t(a text, b int);"
1036 );
1037 }
1038
1039 #[test]
1040 fn add_schema_create_foreign_table() {
1041 assert_snapshot!(apply_code_action(
1042 add_schema,
1043 "create foreign table t$0(a text, b int) server foo;"),
1044 @"create foreign table public.t(a text, b int) server foo;"
1045 );
1046 }
1047
1048 #[test]
1049 fn add_schema_create_function() {
1050 assert_snapshot!(apply_code_action(
1051 add_schema,
1052 "create function f$0() returns int8\n as 'select 1'\n language sql;"),
1053 @"create function public.f() returns int8
1054 as 'select 1'
1055 language sql;"
1056 );
1057 }
1058
1059 #[test]
1060 fn add_schema_create_type() {
1061 assert_snapshot!(apply_code_action(
1062 add_schema,
1063 "create type t$0 as enum ();"),
1064 @"create type public.t as enum ();"
1065 );
1066 }
1067
1068 #[test]
1069 fn add_schema_table_stmt() {
1070 assert_snapshot!(apply_code_action(
1071 add_schema,
1072 "table t$0;"),
1073 @"table public.t;"
1074 );
1075 }
1076
1077 #[test]
1078 fn add_schema_select_from() {
1079 assert_snapshot!(apply_code_action(
1080 add_schema,
1081 "create table t(a text, b int);
1082 select t from t$0;"),
1083 @"create table t(a text, b int);
1084 select t from public.t;"
1085 );
1086 }
1087
1088 #[test]
1089 fn add_schema_select_table_value() {
1090 assert!(code_action_not_applicable(
1093 add_schema,
1094 "create table t(a text, b int);
1095 select t$0 from t;"
1096 ));
1097 }
1098
1099 #[test]
1100 fn add_schema_select_unqualified_column() {
1101 assert!(code_action_not_applicable(
1104 add_schema,
1105 "create table t(a text, b int);
1106 select a$0 from t;"
1107 ));
1108 }
1109
1110 #[test]
1111 fn add_schema_select_qualified_column() {
1112 assert!(code_action_not_applicable(
1115 add_schema,
1116 "create table t(c text);
1117 select t$0.c from t;"
1118 ));
1119 }
1120
1121 #[test]
1122 fn add_schema_with_search_path() {
1123 assert_snapshot!(
1124 apply_code_action(
1125 add_schema,
1126 "
1127set search_path to myschema;
1128create table t$0(a text, b int);"
1129 ),
1130 @"
1131set search_path to myschema;
1132create table myschema.t(a text, b int);"
1133 );
1134 }
1135
1136 #[test]
1137 fn add_schema_not_applicable_with_schema() {
1138 assert!(code_action_not_applicable(
1139 add_schema,
1140 "create table myschema.t$0(a text, b int);"
1141 ));
1142 }
1143
1144 #[test]
1145 fn add_schema_function_call() {
1146 assert_snapshot!(apply_code_action(
1147 add_schema,
1148 "
1149create function f() returns int8
1150 as 'select 1'
1151 language sql;
1152
1153select f$0();"),
1154 @"
1155create function f() returns int8
1156 as 'select 1'
1157 language sql;
1158
1159select public.f();"
1160 );
1161 }
1162
1163 #[test]
1164 fn add_schema_function_call_not_applicable_with_schema() {
1165 assert!(code_action_not_applicable(
1166 add_schema,
1167 "
1168create function f() returns int8 as 'select 1' language sql;
1169select myschema.f$0();"
1170 ));
1171 }
1172
1173 #[test]
1174 fn rewrite_select_as_table_not_applicable_with_distinct() {
1175 assert!(code_action_not_applicable(
1176 rewrite_select_as_table,
1177 "select distinct * from foo$0;"
1178 ));
1179 }
1180
1181 #[test]
1182 fn rewrite_select_as_table_not_applicable_with_columns() {
1183 assert!(code_action_not_applicable(
1184 rewrite_select_as_table,
1185 "select id, name from foo$0;"
1186 ));
1187 }
1188
1189 #[test]
1190 fn rewrite_select_as_table_not_applicable_with_join() {
1191 assert!(code_action_not_applicable(
1192 rewrite_select_as_table,
1193 "select * from foo$0 join bar on foo.id = bar.id;"
1194 ));
1195 }
1196
1197 #[test]
1198 fn rewrite_select_as_table_not_applicable_with_alias() {
1199 assert!(code_action_not_applicable(
1200 rewrite_select_as_table,
1201 "select * from foo$0 f;"
1202 ));
1203 }
1204
1205 #[test]
1206 fn rewrite_select_as_table_not_applicable_with_multiple_tables() {
1207 assert!(code_action_not_applicable(
1208 rewrite_select_as_table,
1209 "select * from foo$0, bar;"
1210 ));
1211 }
1212
1213 #[test]
1214 fn rewrite_select_as_table_not_applicable_on_table() {
1215 assert!(code_action_not_applicable(
1216 rewrite_select_as_table,
1217 "table foo$0;"
1218 ));
1219 }
1220
1221 #[test]
1222 fn quote_identifier_on_name_ref() {
1223 assert_snapshot!(apply_code_action(
1224 quote_identifier,
1225 "select x$0 from t;"),
1226 @r#"select "x" from t;"#
1227 );
1228 }
1229
1230 #[test]
1231 fn quote_identifier_on_name() {
1232 assert_snapshot!(apply_code_action(
1233 quote_identifier,
1234 "create table T(X$0 int);"),
1235 @r#"create table T("x" int);"#
1236 );
1237 }
1238
1239 #[test]
1240 fn quote_identifier_lowercases() {
1241 assert_snapshot!(apply_code_action(
1242 quote_identifier,
1243 "create table T(COL$0 int);"),
1244 @r#"create table T("col" int);"#
1245 );
1246 }
1247
1248 #[test]
1249 fn quote_identifier_not_applicable_when_already_quoted() {
1250 assert!(code_action_not_applicable(
1251 quote_identifier,
1252 r#"select "x"$0 from t;"#
1253 ));
1254 }
1255
1256 #[test]
1257 fn quote_identifier_not_applicable_on_select_keyword() {
1258 assert!(code_action_not_applicable(
1259 quote_identifier,
1260 "sel$0ect x from t;"
1261 ));
1262 }
1263
1264 #[test]
1265 fn quote_identifier_on_keyword_column_name() {
1266 assert_snapshot!(apply_code_action(
1267 quote_identifier,
1268 "select te$0xt from t;"),
1269 @r#"select "text" from t;"#
1270 );
1271 }
1272
1273 #[test]
1274 fn quote_identifier_example_select() {
1275 assert_snapshot!(apply_code_action(
1276 quote_identifier,
1277 "select x$0 from t;"),
1278 @r#"select "x" from t;"#
1279 );
1280 }
1281
1282 #[test]
1283 fn quote_identifier_example_create_table() {
1284 assert_snapshot!(apply_code_action(
1285 quote_identifier,
1286 "create table T(X$0 int);"),
1287 @r#"create table T("x" int);"#
1288 );
1289 }
1290
1291 #[test]
1292 fn unquote_identifier_simple() {
1293 assert_snapshot!(apply_code_action(
1294 unquote_identifier,
1295 r#"select "x"$0 from t;"#),
1296 @"select x from t;"
1297 );
1298 }
1299
1300 #[test]
1301 fn unquote_identifier_with_underscore() {
1302 assert_snapshot!(apply_code_action(
1303 unquote_identifier,
1304 r#"select "user_id"$0 from t;"#),
1305 @"select user_id from t;"
1306 );
1307 }
1308
1309 #[test]
1310 fn unquote_identifier_with_digits() {
1311 assert_snapshot!(apply_code_action(
1312 unquote_identifier,
1313 r#"select "x123"$0 from t;"#),
1314 @"select x123 from t;"
1315 );
1316 }
1317
1318 #[test]
1319 fn unquote_identifier_with_dollar() {
1320 assert_snapshot!(apply_code_action(
1321 unquote_identifier,
1322 r#"select "my_table$1"$0 from t;"#),
1323 @"select my_table$1 from t;"
1324 );
1325 }
1326
1327 #[test]
1328 fn unquote_identifier_starts_with_underscore() {
1329 assert_snapshot!(apply_code_action(
1330 unquote_identifier,
1331 r#"select "_col"$0 from t;"#),
1332 @"select _col from t;"
1333 );
1334 }
1335
1336 #[test]
1337 fn unquote_identifier_starts_with_unicode() {
1338 assert_snapshot!(apply_code_action(
1339 unquote_identifier,
1340 r#"select "é"$0 from t;"#),
1341 @"select é from t;"
1342 );
1343 }
1344
1345 #[test]
1346 fn unquote_identifier_not_applicable() {
1347 assert!(code_action_not_applicable(
1349 unquote_identifier,
1350 r#"select "X"$0 from t;"#
1351 ));
1352 assert!(code_action_not_applicable(
1354 unquote_identifier,
1355 r#"select "Foo"$0 from t;"#
1356 ));
1357 assert!(code_action_not_applicable(
1359 unquote_identifier,
1360 r#"select "my-col"$0 from t;"#
1361 ));
1362 assert!(code_action_not_applicable(
1364 unquote_identifier,
1365 r#"select "123"$0 from t;"#
1366 ));
1367 assert!(code_action_not_applicable(
1369 unquote_identifier,
1370 r#"select "foo bar"$0 from t;"#
1371 ));
1372 assert!(code_action_not_applicable(
1374 unquote_identifier,
1375 r#"select "foo""bar"$0 from t;"#
1376 ));
1377 assert!(code_action_not_applicable(
1379 unquote_identifier,
1380 "select x$0 from t;"
1381 ));
1382 assert!(code_action_not_applicable(
1384 unquote_identifier,
1385 r#"select "my[col]"$0 from t;"#
1386 ));
1387 assert!(code_action_not_applicable(
1389 unquote_identifier,
1390 r#"select "my{}"$0 from t;"#
1391 ));
1392 assert!(code_action_not_applicable(
1394 unquote_identifier,
1395 r#"select "select"$0 from t;"#
1396 ));
1397 }
1398
1399 #[test]
1400 fn unquote_identifier_on_name() {
1401 assert_snapshot!(apply_code_action(
1402 unquote_identifier,
1403 r#"create table T("x"$0 int);"#),
1404 @"create table T(x int);"
1405 );
1406 }
1407
1408 #[test]
1409 fn add_explicit_alias_simple_column() {
1410 assert_snapshot!(apply_code_action(
1411 add_explicit_alias,
1412 "select col_na$0me from t;"),
1413 @"select col_name as col_name from t;"
1414 );
1415 }
1416
1417 #[test]
1418 fn add_explicit_alias_quoted_identifier() {
1419 assert_snapshot!(apply_code_action(
1420 add_explicit_alias,
1421 r#"select "b"$0 from t;"#),
1422 @r#"select "b" as b from t;"#
1423 );
1424 }
1425
1426 #[test]
1427 fn add_explicit_alias_field_expr() {
1428 assert_snapshot!(apply_code_action(
1429 add_explicit_alias,
1430 "select t.col$0umn from t;"),
1431 @"select t.column as column from t;"
1432 );
1433 }
1434
1435 #[test]
1436 fn add_explicit_alias_function_call() {
1437 assert_snapshot!(apply_code_action(
1438 add_explicit_alias,
1439 "select cou$0nt(*) from t;"),
1440 @"select count(*) as count from t;"
1441 );
1442 }
1443
1444 #[test]
1445 fn add_explicit_alias_cast_to_type() {
1446 assert_snapshot!(apply_code_action(
1447 add_explicit_alias,
1448 "select '1'::bigi$0nt from t;"),
1449 @"select '1'::bigint as int8 from t;"
1450 );
1451 }
1452
1453 #[test]
1454 fn add_explicit_alias_cast_column() {
1455 assert_snapshot!(apply_code_action(
1456 add_explicit_alias,
1457 "select col_na$0me::text from t;"),
1458 @"select col_name::text as col_name from t;"
1459 );
1460 }
1461
1462 #[test]
1463 fn add_explicit_alias_case_expr() {
1464 assert_snapshot!(apply_code_action(
1465 add_explicit_alias,
1466 "select ca$0se when true then 'a' end from t;"),
1467 @"select case when true then 'a' end as case from t;"
1468 );
1469 }
1470
1471 #[test]
1472 fn add_explicit_alias_case_with_else() {
1473 assert_snapshot!(apply_code_action(
1474 add_explicit_alias,
1475 "select ca$0se when true then 'a' else now()::text end from t;"),
1476 @"select case when true then 'a' else now()::text end as now from t;"
1477 );
1478 }
1479
1480 #[test]
1481 fn add_explicit_alias_array() {
1482 assert_snapshot!(apply_code_action(
1483 add_explicit_alias,
1484 "select arr$0ay[1, 2, 3] from t;"),
1485 @"select array[1, 2, 3] as array from t;"
1486 );
1487 }
1488
1489 #[test]
1490 fn add_explicit_alias_not_applicable_already_has_alias() {
1491 assert!(code_action_not_applicable(
1492 add_explicit_alias,
1493 "select col_name$0 as foo from t;"
1494 ));
1495 }
1496
1497 #[test]
1498 fn add_explicit_alias_unknown_column() {
1499 assert_snapshot!(apply_code_action(
1500 add_explicit_alias,
1501 "select 1 $0+ 2 from t;"),
1502 @r#"select 1 + 2 as "?column?" from t;"#
1503 );
1504 }
1505
1506 #[test]
1507 fn add_explicit_alias_not_applicable_star() {
1508 assert!(code_action_not_applicable(
1509 add_explicit_alias,
1510 "select $0* from t;"
1511 ));
1512 }
1513
1514 #[test]
1515 fn add_explicit_alias_literal() {
1516 assert_snapshot!(apply_code_action(
1517 add_explicit_alias,
1518 "select 'foo$0' from t;"),
1519 @r#"select 'foo' as "?column?" from t;"#
1520 );
1521 }
1522
1523 #[test]
1524 fn remove_redundant_alias_simple() {
1525 assert_snapshot!(apply_code_action(
1526 remove_redundant_alias,
1527 "select col_name as col_na$0me from t;"),
1528 @"select col_name from t;"
1529 );
1530 }
1531
1532 #[test]
1533 fn remove_redundant_alias_quoted() {
1534 assert_snapshot!(apply_code_action(
1535 remove_redundant_alias,
1536 r#"select "x"$0 as x from t;"#),
1537 @r#"select "x" from t;"#
1538 );
1539 }
1540
1541 #[test]
1542 fn remove_redundant_alias_case_insensitive() {
1543 assert_snapshot!(apply_code_action(
1544 remove_redundant_alias,
1545 "select col_name$0 as COL_NAME from t;"),
1546 @"select col_name from t;"
1547 );
1548 }
1549
1550 #[test]
1551 fn remove_redundant_alias_function() {
1552 assert_snapshot!(apply_code_action(
1553 remove_redundant_alias,
1554 "select count(*)$0 as count from t;"),
1555 @"select count(*) from t;"
1556 );
1557 }
1558
1559 #[test]
1560 fn remove_redundant_alias_field_expr() {
1561 assert_snapshot!(apply_code_action(
1562 remove_redundant_alias,
1563 "select t.col$0umn as column from t;"),
1564 @"select t.column from t;"
1565 );
1566 }
1567
1568 #[test]
1569 fn remove_redundant_alias_not_applicable_different_name() {
1570 assert!(code_action_not_applicable(
1571 remove_redundant_alias,
1572 "select col_name$0 as foo from t;"
1573 ));
1574 }
1575
1576 #[test]
1577 fn remove_redundant_alias_not_applicable_no_alias() {
1578 assert!(code_action_not_applicable(
1579 remove_redundant_alias,
1580 "select col_name$0 from t;"
1581 ));
1582 }
1583
1584 #[test]
1585 fn rewrite_cast_to_double_colon_simple() {
1586 assert_snapshot!(apply_code_action(
1587 rewrite_cast_to_double_colon,
1588 "select ca$0st(foo as text) from t;"),
1589 @"select foo::text from t;"
1590 );
1591 }
1592
1593 #[test]
1594 fn rewrite_cast_to_double_colon_on_column() {
1595 assert_snapshot!(apply_code_action(
1596 rewrite_cast_to_double_colon,
1597 "select cast(col_na$0me as int) from t;"),
1598 @"select col_name::int from t;"
1599 );
1600 }
1601
1602 #[test]
1603 fn rewrite_cast_to_double_colon_on_type() {
1604 assert_snapshot!(apply_code_action(
1605 rewrite_cast_to_double_colon,
1606 "select cast(x as bigi$0nt) from t;"),
1607 @"select x::bigint from t;"
1608 );
1609 }
1610
1611 #[test]
1612 fn rewrite_cast_to_double_colon_qualified_type() {
1613 assert_snapshot!(apply_code_action(
1614 rewrite_cast_to_double_colon,
1615 "select cast(x as pg_cata$0log.text) from t;"),
1616 @"select x::pg_catalog.text from t;"
1617 );
1618 }
1619
1620 #[test]
1621 fn rewrite_cast_to_double_colon_expression() {
1622 assert_snapshot!(apply_code_action(
1623 rewrite_cast_to_double_colon,
1624 "select ca$0st(1 + 2 as bigint) from t;"),
1625 @"select 1 + 2::bigint from t;"
1626 );
1627 }
1628
1629 #[test]
1630 fn rewrite_cast_to_double_colon_not_applicable_already_double_colon() {
1631 assert!(code_action_not_applicable(
1632 rewrite_cast_to_double_colon,
1633 "select foo::te$0xt from t;"
1634 ));
1635 }
1636
1637 #[test]
1638 fn rewrite_cast_to_double_colon_not_applicable_outside_cast() {
1639 assert!(code_action_not_applicable(
1640 rewrite_cast_to_double_colon,
1641 "select fo$0o from t;"
1642 ));
1643 }
1644
1645 #[test]
1646 fn rewrite_double_colon_to_cast_simple() {
1647 assert_snapshot!(apply_code_action(
1648 rewrite_double_colon_to_cast,
1649 "select foo::te$0xt from t;"),
1650 @"select cast(foo as text) from t;"
1651 );
1652 }
1653
1654 #[test]
1655 fn rewrite_double_colon_to_cast_on_column() {
1656 assert_snapshot!(apply_code_action(
1657 rewrite_double_colon_to_cast,
1658 "select col_na$0me::int from t;"),
1659 @"select cast(col_name as int) from t;"
1660 );
1661 }
1662
1663 #[test]
1664 fn rewrite_double_colon_to_cast_on_type() {
1665 assert_snapshot!(apply_code_action(
1666 rewrite_double_colon_to_cast,
1667 "select x::bigi$0nt from t;"),
1668 @"select cast(x as bigint) from t;"
1669 );
1670 }
1671
1672 #[test]
1673 fn rewrite_double_colon_to_cast_qualified_type() {
1674 assert_snapshot!(apply_code_action(
1675 rewrite_double_colon_to_cast,
1676 "select x::pg_cata$0log.text from t;"),
1677 @"select cast(x as pg_catalog.text) from t;"
1678 );
1679 }
1680
1681 #[test]
1682 fn rewrite_double_colon_to_cast_expression() {
1683 assert_snapshot!(apply_code_action(
1684 rewrite_double_colon_to_cast,
1685 "select 1 + 2::bigi$0nt from t;"),
1686 @"select 1 + cast(2 as bigint) from t;"
1687 );
1688 }
1689
1690 #[test]
1691 fn rewrite_double_colon_to_cast_not_applicable_already_cast() {
1692 assert!(code_action_not_applicable(
1693 rewrite_double_colon_to_cast,
1694 "select ca$0st(foo as text) from t;"
1695 ));
1696 }
1697
1698 #[test]
1699 fn rewrite_double_colon_to_cast_not_applicable_outside_cast() {
1700 assert!(code_action_not_applicable(
1701 rewrite_double_colon_to_cast,
1702 "select fo$0o from t;"
1703 ));
1704 }
1705
1706 #[test]
1707 fn rewrite_values_as_select_simple() {
1708 assert_snapshot!(
1709 apply_code_action(rewrite_values_as_select, "valu$0es (1, 'one'), (2, 'two');"),
1710 @r"
1711 select 1 as column1, 'one' as column2
1712 union all
1713 select 2, 'two';
1714 "
1715 );
1716 }
1717
1718 #[test]
1719 fn rewrite_values_as_select_single_row() {
1720 assert_snapshot!(
1721 apply_code_action(rewrite_values_as_select, "val$0ues (1, 2, 3);"),
1722 @"select 1 as column1, 2 as column2, 3 as column3;"
1723 );
1724 }
1725
1726 #[test]
1727 fn rewrite_values_as_select_single_column() {
1728 assert_snapshot!(
1729 apply_code_action(rewrite_values_as_select, "values$0 (1);"),
1730 @"select 1 as column1;"
1731 );
1732 }
1733
1734 #[test]
1735 fn rewrite_values_as_select_multiple_rows() {
1736 assert_snapshot!(
1737 apply_code_action(rewrite_values_as_select, "values (1, 2), (3, 4), (5, 6$0);"),
1738 @r"
1739 select 1 as column1, 2 as column2
1740 union all
1741 select 3, 4
1742 union all
1743 select 5, 6;
1744 "
1745 );
1746 }
1747
1748 #[test]
1749 fn rewrite_values_as_select_with_clause() {
1750 assert_snapshot!(
1751 apply_code_action(
1752 rewrite_values_as_select,
1753 "with cte as (select 1) val$0ues (1, 'one'), (2, 'two');"
1754 ),
1755 @r"
1756 with cte as (select 1) select 1 as column1, 'one' as column2
1757 union all
1758 select 2, 'two';
1759 "
1760 );
1761 }
1762
1763 #[test]
1764 fn rewrite_values_as_select_complex_expressions() {
1765 assert_snapshot!(
1766 apply_code_action(
1767 rewrite_values_as_select,
1768 "values (1 + 2, 'test'::text$0, array[1,2]);"
1769 ),
1770 @"select 1 + 2 as column1, 'test'::text as column2, array[1,2] as column3;"
1771 );
1772 }
1773
1774 #[test]
1775 fn rewrite_values_as_select_on_values_keyword() {
1776 assert_snapshot!(
1777 apply_code_action(rewrite_values_as_select, "val$0ues (1, 2);"),
1778 @"select 1 as column1, 2 as column2;"
1779 );
1780 }
1781
1782 #[test]
1783 fn rewrite_values_as_select_on_row_content() {
1784 assert_snapshot!(
1785 apply_code_action(rewrite_values_as_select, "values (1$0, 2), (3, 4);"),
1786 @r"
1787 select 1 as column1, 2 as column2
1788 union all
1789 select 3, 4;
1790 "
1791 );
1792 }
1793
1794 #[test]
1795 fn rewrite_values_as_select_not_applicable_on_select() {
1796 assert!(code_action_not_applicable(
1797 rewrite_values_as_select,
1798 "sel$0ect 1;"
1799 ));
1800 }
1801
1802 #[test]
1803 fn rewrite_select_as_values_simple() {
1804 assert_snapshot!(
1805 apply_code_action(
1806 rewrite_select_as_values,
1807 "select 1 as column1, 'one' as column2 union all$0 select 2, 'two';"
1808 ),
1809 @"values (1, 'one'), (2, 'two');"
1810 );
1811 }
1812
1813 #[test]
1814 fn rewrite_select_as_values_multiple_rows() {
1815 assert_snapshot!(
1816 apply_code_action(
1817 rewrite_select_as_values,
1818 "select 1 as column1, 2 as column2 union$0 all select 3, 4 union all select 5, 6;"
1819 ),
1820 @"values (1, 2), (3, 4), (5, 6);"
1821 );
1822 }
1823
1824 #[test]
1825 fn rewrite_select_as_values_multiple_rows_cursor_on_second_union() {
1826 assert_snapshot!(
1827 apply_code_action(
1828 rewrite_select_as_values,
1829 "select 1 as column1, 2 as column2 union all select 3, 4 union$0 all select 5, 6;"
1830 ),
1831 @"values (1, 2), (3, 4), (5, 6);"
1832 );
1833 }
1834
1835 #[test]
1836 fn rewrite_select_as_values_single_column() {
1837 assert_snapshot!(
1838 apply_code_action(
1839 rewrite_select_as_values,
1840 "select 1 as column1$0 union all select 2;"
1841 ),
1842 @"values (1), (2);"
1843 );
1844 }
1845
1846 #[test]
1847 fn rewrite_select_as_values_with_clause() {
1848 assert_snapshot!(
1849 apply_code_action(
1850 rewrite_select_as_values,
1851 "with cte as (select 1) select 1 as column1, 'one' as column2 uni$0on all select 2, 'two';"
1852 ),
1853 @"with cte as (select 1) values (1, 'one'), (2, 'two');"
1854 );
1855 }
1856
1857 #[test]
1858 fn rewrite_select_as_values_complex_expressions() {
1859 assert_snapshot!(
1860 apply_code_action(
1861 rewrite_select_as_values,
1862 "select 1 + 2 as column1, 'test'::text as column2$0 union all select 3 * 4, array[1,2]::text;"
1863 ),
1864 @"values (1 + 2, 'test'::text), (3 * 4, array[1,2]::text);"
1865 );
1866 }
1867
1868 #[test]
1869 fn rewrite_select_as_values_single_select() {
1870 assert_snapshot!(
1871 apply_code_action(
1872 rewrite_select_as_values,
1873 "select 1 as column1, 2 as column2$0;"
1874 ),
1875 @"values (1, 2);"
1876 );
1877 }
1878
1879 #[test]
1880 fn rewrite_select_as_values_single_select_with_clause() {
1881 assert_snapshot!(
1882 apply_code_action(
1883 rewrite_select_as_values,
1884 "with cte as (select 1) select 1 as column1$0, 'test' as column2;"
1885 ),
1886 @"with cte as (select 1) values (1, 'test');"
1887 );
1888 }
1889
1890 #[test]
1891 fn rewrite_select_as_values_not_applicable_union_without_all() {
1892 assert!(code_action_not_applicable(
1893 rewrite_select_as_values,
1894 "select 1 as column1 union$0 select 2;"
1895 ));
1896 }
1897
1898 #[test]
1899 fn rewrite_select_as_values_not_applicable_wrong_column_names() {
1900 assert!(code_action_not_applicable(
1901 rewrite_select_as_values,
1902 "select 1 as foo, 2 as bar union all$0 select 3, 4;"
1903 ));
1904 }
1905
1906 #[test]
1907 fn rewrite_select_as_values_not_applicable_missing_aliases() {
1908 assert!(code_action_not_applicable(
1909 rewrite_select_as_values,
1910 "select 1, 2 union all$0 select 3, 4;"
1911 ));
1912 }
1913
1914 #[test]
1915 fn rewrite_select_as_values_case_insensitive_column_names() {
1916 assert_snapshot!(
1917 apply_code_action(
1918 rewrite_select_as_values,
1919 "select 1 as COLUMN1, 2 as CoLuMn2 union all$0 select 3, 4;"
1920 ),
1921 @"values (1, 2), (3, 4);"
1922 );
1923 }
1924
1925 #[test]
1926 fn rewrite_select_as_values_not_applicable_with_values() {
1927 assert!(code_action_not_applicable(
1928 rewrite_select_as_values,
1929 "select 1 as column1, 2 as column2 union all$0 values (3, 4);"
1930 ));
1931 }
1932
1933 #[test]
1934 fn rewrite_select_as_values_not_applicable_with_table() {
1935 assert!(code_action_not_applicable(
1936 rewrite_select_as_values,
1937 "select 1 as column1, 2 as column2 union all$0 table foo;"
1938 ));
1939 }
1940
1941 #[test]
1942 fn rewrite_select_as_values_not_applicable_intersect() {
1943 assert!(code_action_not_applicable(
1944 rewrite_select_as_values,
1945 "select 1 as column1, 2 as column2 inter$0sect select 3, 4;"
1946 ));
1947 }
1948
1949 #[test]
1950 fn rewrite_select_as_values_not_applicable_except() {
1951 assert!(code_action_not_applicable(
1952 rewrite_select_as_values,
1953 "select 1 as column1, 2 as column2 exc$0ept select 3, 4;"
1954 ));
1955 }
1956}