1use itertools::Itertools;
2use rowan::{TextRange, TextSize};
3use salsa::Database as Db;
4use squawk_linter::Edit;
5use squawk_syntax::{
6 SyntaxKind, SyntaxToken,
7 ast::{self, AstNode},
8};
9use std::iter;
10
11use crate::{
12 binder,
13 column_name::ColumnName,
14 db::{File, parse},
15 offsets::token_from_offset,
16 quote::{quote_column_alias, unquote_ident},
17 symbols::Name,
18};
19
20#[derive(Debug, Clone, PartialEq, Eq)]
21pub enum ActionKind {
22 QuickFix,
23 RefactorRewrite,
24}
25
26#[derive(Debug, Clone, PartialEq, Eq)]
27pub struct CodeAction {
28 pub title: String,
29 pub edits: Vec<Edit>,
30 pub kind: ActionKind,
31}
32
33#[salsa::tracked]
34pub fn code_actions(db: &dyn Db, file: File, offset: TextSize) -> Option<Vec<CodeAction>> {
35 let parse = parse(db, file);
36 let source_file = parse.tree();
37
38 let mut actions = vec![];
39 rewrite_as_regular_string(&mut actions, &source_file, offset);
40 rewrite_as_dollar_quoted_string(&mut actions, &source_file, offset);
41 remove_else_clause(&mut actions, &source_file, offset);
42 rewrite_table_as_select(&mut actions, &source_file, offset);
43 rewrite_select_as_table(&mut actions, &source_file, offset);
44 rewrite_from(&mut actions, &source_file, offset);
45 rewrite_leading_from(&mut actions, &source_file, offset);
46 rewrite_values_as_select(&mut actions, &source_file, offset);
47 rewrite_select_as_values(&mut actions, &source_file, offset);
48 add_schema(&mut actions, &source_file, offset);
49 quote_identifier(&mut actions, &source_file, offset);
50 unquote_identifier(&mut actions, &source_file, offset);
51 add_explicit_alias(&mut actions, &source_file, offset);
52 remove_redundant_alias(&mut actions, &source_file, offset);
53 rewrite_cast_to_double_colon(&mut actions, &source_file, offset);
54 rewrite_double_colon_to_cast(&mut actions, &source_file, offset);
55 rewrite_between_as_binary_expression(&mut actions, &source_file, offset);
56 rewrite_not_equals_operator(&mut actions, &source_file, offset);
57 rewrite_timestamp_type(&mut actions, &source_file, offset);
58 Some(actions)
59}
60
61fn rewrite_as_regular_string(
62 actions: &mut Vec<CodeAction>,
63 file: &ast::SourceFile,
64 offset: TextSize,
65) -> Option<()> {
66 let dollar_string = file
67 .syntax()
68 .token_at_offset(offset)
69 .find(|token| token.kind() == SyntaxKind::DOLLAR_QUOTED_STRING)?;
70
71 let replacement = dollar_quoted_to_string(dollar_string.text())?;
72 actions.push(CodeAction {
73 title: "Rewrite as regular string".to_owned(),
74 edits: vec![Edit::replace(dollar_string.text_range(), replacement)],
75 kind: ActionKind::RefactorRewrite,
76 });
77
78 Some(())
79}
80
81fn rewrite_as_dollar_quoted_string(
82 actions: &mut Vec<CodeAction>,
83 file: &ast::SourceFile,
84 offset: TextSize,
85) -> Option<()> {
86 let string = file
87 .syntax()
88 .token_at_offset(offset)
89 .find(|token| token.kind() == SyntaxKind::STRING)?;
90
91 let replacement = string_to_dollar_quoted(string.text())?;
92 actions.push(CodeAction {
93 title: "Rewrite as dollar-quoted string".to_owned(),
94 edits: vec![Edit::replace(string.text_range(), replacement)],
95 kind: ActionKind::RefactorRewrite,
96 });
97
98 Some(())
99}
100
101fn string_to_dollar_quoted(text: &str) -> Option<String> {
102 let normalized = normalize_single_quoted_string(text)?;
103 let delimiter = dollar_delimiter(&normalized)?;
104 let boundary = format!("${}$", delimiter);
105 Some(format!("{boundary}{normalized}{boundary}"))
106}
107
108fn dollar_quoted_to_string(text: &str) -> Option<String> {
109 debug_assert!(text.starts_with('$'));
110 let (delimiter, content) = split_dollar_quoted(text)?;
111 let boundary = format!("${}$", delimiter);
112
113 if !text.starts_with(&boundary) || !text.ends_with(&boundary) {
114 return None;
115 }
116
117 let escaped = content.replace('\'', "''");
119 Some(format!("'{}'", escaped))
120}
121
122fn split_dollar_quoted(text: &str) -> Option<(String, &str)> {
123 debug_assert!(text.starts_with('$'));
124 let second_dollar = text[1..].find('$')?;
125 let delimiter = &text[1..=second_dollar];
127 let boundary = format!("${}$", delimiter);
128
129 if !text.ends_with(&boundary) {
130 return None;
131 }
132
133 let start = boundary.len();
134 let end = text.len().checked_sub(boundary.len())?;
135 let content = text.get(start..end)?;
136 Some((delimiter.to_owned(), content))
137}
138
139fn normalize_single_quoted_string(text: &str) -> Option<String> {
140 let body = text.strip_prefix('\'')?.strip_suffix('\'')?;
141 return Some(body.replace("''", "'"));
142}
143
144fn dollar_delimiter(content: &str) -> Option<String> {
145 if !content.contains("$$") && !content.ends_with('$') {
148 return Some("".to_owned());
149 }
150
151 let mut delim = "q".to_owned();
152 for idx in 0..10 {
154 if !content.contains(&format!("${}$", delim)) {
155 return Some(delim);
156 }
157 delim.push_str(&idx.to_string());
158 }
159 None
160}
161
162fn remove_else_clause(
163 actions: &mut Vec<CodeAction>,
164 file: &ast::SourceFile,
165 offset: TextSize,
166) -> Option<()> {
167 let else_token = file
168 .syntax()
169 .token_at_offset(offset)
170 .find(|x| x.kind() == SyntaxKind::ELSE_KW)?;
171 let parent = else_token.parent()?;
172 let else_clause = ast::ElseClause::cast(parent)?;
173
174 let mut edits = vec![];
175 edits.push(Edit::delete(else_clause.syntax().text_range()));
176 if let Some(token) = else_token.prev_token()
177 && token.kind() == SyntaxKind::WHITESPACE
178 {
179 edits.push(Edit::delete(token.text_range()));
180 }
181
182 actions.push(CodeAction {
183 title: "Remove `else` clause".to_owned(),
184 edits,
185 kind: ActionKind::RefactorRewrite,
186 });
187 Some(())
188}
189
190fn rewrite_table_as_select(
191 actions: &mut Vec<CodeAction>,
192 file: &ast::SourceFile,
193 offset: TextSize,
194) -> Option<()> {
195 let token = token_from_offset(file, offset)?;
196 let table = token.parent_ancestors().find_map(ast::Table::cast)?;
197
198 let relation_name = table.relation_name()?;
199 let table_name = relation_name.syntax().text();
200
201 let replacement = format!("select * from {}", table_name);
202
203 actions.push(CodeAction {
204 title: "Rewrite as `select`".to_owned(),
205 edits: vec![Edit::replace(table.syntax().text_range(), replacement)],
206 kind: ActionKind::RefactorRewrite,
207 });
208
209 Some(())
210}
211
212fn rewrite_select_as_table(
213 actions: &mut Vec<CodeAction>,
214 file: &ast::SourceFile,
215 offset: TextSize,
216) -> Option<()> {
217 let token = token_from_offset(file, offset)?;
218 let select = token.parent_ancestors().find_map(ast::Select::cast)?;
219
220 if !can_transform_select_to_table(&select) {
221 return None;
222 }
223
224 let from_clause = select.from_clause()?;
225 let from_item = from_clause.from_items().next()?;
226
227 let table_name = if let Some(name_ref) = from_item.name_ref() {
228 name_ref.syntax().text().to_string()
229 } else if let Some(field_expr) = from_item.field_expr() {
230 field_expr.syntax().text().to_string()
231 } else {
232 return None;
233 };
234
235 let replacement = format!("table {}", table_name);
236
237 actions.push(CodeAction {
238 title: "Rewrite as `table`".to_owned(),
239 edits: vec![Edit::replace(select.syntax().text_range(), replacement)],
240 kind: ActionKind::RefactorRewrite,
241 });
242
243 Some(())
244}
245
246fn rewrite_from(
247 actions: &mut Vec<CodeAction>,
248 file: &ast::SourceFile,
249 offset: TextSize,
250) -> Option<()> {
251 let token = token_from_offset(file, offset)?;
252 let select = token.parent_ancestors().find_map(ast::Select::cast)?;
253
254 if select.select_clause().is_some() {
255 return None;
256 }
257
258 select.from_clause()?;
259
260 actions.push(CodeAction {
261 title: "Insert leading `select *`".to_owned(),
262 edits: vec![Edit::insert(
263 "select * ".to_owned(),
264 select.syntax().text_range().start(),
265 )],
266 kind: ActionKind::QuickFix,
267 });
268
269 Some(())
270}
271
272fn rewrite_leading_from(
273 actions: &mut Vec<CodeAction>,
274 file: &ast::SourceFile,
275 offset: TextSize,
276) -> Option<()> {
277 let token = token_from_offset(file, offset)?;
278 let select = token.parent_ancestors().find_map(ast::Select::cast)?;
279
280 let from_clause = select.from_clause()?;
281 let select_clause = select.select_clause()?;
282
283 if from_clause.syntax().text_range().start() >= select_clause.syntax().text_range().start() {
284 return None;
285 }
286
287 let select_text = select_clause.syntax().text().to_string();
288
289 let mut delete_start = select_clause.syntax().text_range().start();
290 if let Some(prev) = select_clause.syntax().prev_sibling_or_token()
291 && prev.kind() == SyntaxKind::WHITESPACE
292 {
293 delete_start = prev.text_range().start();
294 }
295 let select_with_ws = TextRange::new(delete_start, select_clause.syntax().text_range().end());
296
297 actions.push(CodeAction {
298 title: "Swap `from` and `select` clauses".to_owned(),
299 edits: vec![
300 Edit::delete(select_with_ws),
301 Edit::insert(
302 format!("{} ", select_text),
303 from_clause.syntax().text_range().start(),
304 ),
305 ],
306 kind: ActionKind::QuickFix,
307 });
308
309 Some(())
310}
311
312fn can_transform_select_to_table(select: &ast::Select) -> bool {
319 if select.with_clause().is_some()
320 || select.where_clause().is_some()
321 || select.group_by_clause().is_some()
322 || select.having_clause().is_some()
323 || select.window_clause().is_some()
324 || select.order_by_clause().is_some()
325 || select.limit_clause().is_some()
326 || select.fetch_clause().is_some()
327 || select.offset_clause().is_some()
328 || select.filter_clause().is_some()
329 || select.locking_clauses().next().is_some()
330 {
331 return false;
332 }
333
334 let Some(select_clause) = select.select_clause() else {
335 return false;
336 };
337
338 if select_clause.distinct_clause().is_some() {
339 return false;
340 }
341
342 let Some(target_list) = select_clause.target_list() else {
343 return false;
344 };
345
346 let mut targets = target_list.targets();
347 let Some(target) = targets.next() else {
348 return false;
349 };
350
351 if targets.next().is_some() {
352 return false;
353 }
354
355 if target.expr().is_some() || target.star_token().is_none() {
357 return false;
358 }
359
360 let Some(from_clause) = select.from_clause() else {
361 return false;
362 };
363
364 let mut from_items = from_clause.from_items();
365 let Some(from_item) = from_items.next() else {
366 return false;
367 };
368
369 if from_items.next().is_some() || from_clause.join_exprs().next().is_some() {
371 return false;
372 }
373
374 if from_item.alias().is_some()
375 || from_item.tablesample_clause().is_some()
376 || from_item.only_token().is_some()
377 || from_item.lateral_token().is_some()
378 || from_item.star_token().is_some()
379 || from_item.call_expr().is_some()
380 || from_item.paren_select().is_some()
381 || from_item.json_table().is_some()
382 || from_item.xml_table().is_some()
383 || from_item.cast_expr().is_some()
384 {
385 return false;
386 }
387
388 from_item.name_ref().is_some() || from_item.field_expr().is_some()
390}
391
392fn quote_identifier(
393 actions: &mut Vec<CodeAction>,
394 file: &ast::SourceFile,
395 offset: TextSize,
396) -> Option<()> {
397 let token = token_from_offset(file, offset)?;
398 let parent = token.parent()?;
399
400 let name_node = if let Some(name) = ast::Name::cast(parent.clone()) {
401 name.syntax().clone()
402 } else if let Some(name_ref) = ast::NameRef::cast(parent) {
403 name_ref.syntax().clone()
404 } else {
405 return None;
406 };
407
408 let text = name_node.text().to_string();
409
410 if text.starts_with('"') {
411 return None;
412 }
413
414 let quoted = format!(r#""{}""#, text.to_lowercase());
415
416 actions.push(CodeAction {
417 title: "Quote identifier".to_owned(),
418 edits: vec![Edit::replace(name_node.text_range(), quoted)],
419 kind: ActionKind::RefactorRewrite,
420 });
421
422 Some(())
423}
424
425fn unquote_identifier(
426 actions: &mut Vec<CodeAction>,
427 file: &ast::SourceFile,
428 offset: TextSize,
429) -> Option<()> {
430 let token = token_from_offset(file, offset)?;
431 let parent = token.parent()?;
432
433 let name_node = if let Some(name) = ast::Name::cast(parent.clone()) {
434 name.syntax().clone()
435 } else if let Some(name_ref) = ast::NameRef::cast(parent) {
436 name_ref.syntax().clone()
437 } else {
438 return None;
439 };
440
441 let unquoted = unquote_ident(&name_node)?;
442
443 actions.push(CodeAction {
444 title: "Unquote identifier".to_owned(),
445 edits: vec![Edit::replace(name_node.text_range(), unquoted)],
446 kind: ActionKind::RefactorRewrite,
447 });
448
449 Some(())
450}
451
452fn add_explicit_alias(
456 actions: &mut Vec<CodeAction>,
457 file: &ast::SourceFile,
458 offset: TextSize,
459) -> Option<()> {
460 let token = token_from_offset(file, offset)?;
461 let target = token.parent_ancestors().find_map(ast::Target::cast)?;
462
463 if target.as_name().is_some() {
464 return None;
465 }
466
467 if let Some(ast::Expr::FieldExpr(field_expr)) = target.expr()
468 && field_expr.star_token().is_some()
469 {
470 return None;
471 }
472
473 let alias = ColumnName::from_target(target.clone()).and_then(|c| c.0.to_string())?;
474
475 let expr_end = target.expr().map(|e| e.syntax().text_range().end())?;
476
477 let quoted_alias = quote_column_alias(&alias);
478 let replacement = format!(" as {}", quoted_alias);
481
482 actions.push(CodeAction {
483 title: "Add explicit alias".to_owned(),
484 edits: vec![Edit::insert(replacement, expr_end)],
485 kind: ActionKind::RefactorRewrite,
486 });
487
488 Some(())
489}
490
491fn remove_redundant_alias(
492 actions: &mut Vec<CodeAction>,
493 file: &ast::SourceFile,
494 offset: TextSize,
495) -> Option<()> {
496 let token = token_from_offset(file, offset)?;
497 let target = token.parent_ancestors().find_map(ast::Target::cast)?;
498
499 let as_name = target.as_name()?;
500 let (inferred_column, _) = ColumnName::inferred_from_target(target.clone())?;
501 let inferred_column_alias = inferred_column.to_string()?;
502
503 let alias = as_name.name()?;
504
505 if Name::from_node(&alias) != Name::from_string(inferred_column_alias) {
506 return None;
507 }
508
509 let expr_end = target.expr()?.syntax().text_range().end();
517 let alias_end = as_name.syntax().text_range().end();
518
519 actions.push(CodeAction {
520 title: "Remove redundant alias".to_owned(),
521 edits: vec![Edit::delete(TextRange::new(expr_end, alias_end))],
522 kind: ActionKind::QuickFix,
523 });
524
525 Some(())
526}
527
528fn add_schema(
529 actions: &mut Vec<CodeAction>,
530 file: &ast::SourceFile,
531 offset: TextSize,
532) -> Option<()> {
533 let token = token_from_offset(file, offset)?;
534 let range = token.parent_ancestors().find_map(|node| {
535 if let Some(path) = ast::Path::cast(node.clone()) {
536 if path.qualifier().is_some() {
537 return None;
538 }
539 return Some(path.syntax().text_range());
540 }
541 if let Some(from_item) = ast::FromItem::cast(node.clone()) {
542 let name_ref = from_item.name_ref()?;
543 return Some(name_ref.syntax().text_range());
544 }
545 if let Some(call_expr) = ast::CallExpr::cast(node) {
546 let ast::Expr::NameRef(name_ref) = call_expr.expr()? else {
547 return None;
548 };
549 return Some(name_ref.syntax().text_range());
550 }
551 None
552 })?;
553
554 if !range.contains(offset) {
555 return None;
556 }
557
558 let position = token.text_range().start();
559 let binder = binder::bind(file);
561 let schema = binder.search_path_at(position).first()?.to_string();
571 let replacement = format!("{}.", schema);
572
573 actions.push(CodeAction {
574 title: "Add schema".to_owned(),
575 edits: vec![Edit::insert(replacement, position)],
576 kind: ActionKind::RefactorRewrite,
577 });
578
579 Some(())
580}
581
582fn rewrite_cast_to_double_colon(
583 actions: &mut Vec<CodeAction>,
584 file: &ast::SourceFile,
585 offset: TextSize,
586) -> Option<()> {
587 let token = token_from_offset(file, offset)?;
588 let cast_expr = token.parent_ancestors().find_map(ast::CastExpr::cast)?;
589
590 if cast_expr.colon_colon().is_some() {
591 return None;
592 }
593
594 let expr = cast_expr.expr()?;
595 let ty = cast_expr.ty()?;
596
597 let expr_text = expr.syntax().text();
598 let type_text = ty.syntax().text();
599
600 let replacement = format!("{}::{}", expr_text, type_text);
601
602 actions.push(CodeAction {
603 title: "Rewrite as cast operator `::`".to_owned(),
604 edits: vec![Edit::replace(cast_expr.syntax().text_range(), replacement)],
605 kind: ActionKind::RefactorRewrite,
606 });
607
608 Some(())
609}
610
611fn rewrite_double_colon_to_cast(
612 actions: &mut Vec<CodeAction>,
613 file: &ast::SourceFile,
614 offset: TextSize,
615) -> Option<()> {
616 let token = token_from_offset(file, offset)?;
617 let cast_expr = token.parent_ancestors().find_map(ast::CastExpr::cast)?;
618
619 if cast_expr.cast_token().is_some() {
620 return None;
621 }
622
623 let expr = cast_expr.expr()?;
624 let ty = cast_expr.ty()?;
625
626 let expr_text = expr.syntax().text();
627 let type_text = ty.syntax().text();
628
629 let replacement = format!("cast({} as {})", expr_text, type_text);
630
631 actions.push(CodeAction {
632 title: "Rewrite as cast function `cast()`".to_owned(),
633 edits: vec![Edit::replace(cast_expr.syntax().text_range(), replacement)],
634 kind: ActionKind::RefactorRewrite,
635 });
636
637 Some(())
638}
639
640fn rewrite_between_as_binary_expression(
641 actions: &mut Vec<CodeAction>,
642 file: &ast::SourceFile,
643 offset: TextSize,
644) -> Option<()> {
645 let token = token_from_offset(file, offset)?;
646 let between_expr = token.parent_ancestors().find_map(ast::BetweenExpr::cast)?;
647
648 let target = between_expr.target()?;
649 let start = between_expr.start()?;
650 let end = between_expr.end()?;
651
652 let is_not = between_expr.not_token().is_some();
653 let is_symmetric = between_expr.symmetric_token().is_some();
654
655 let target_text = target.syntax().text();
656 let start_text = start.syntax().text();
657 let end_text = end.syntax().text();
658
659 let replacement = match (is_not, is_symmetric) {
660 (false, false) => {
661 format!("{target_text} >= {start_text} and {target_text} <= {end_text}")
662 }
663 (true, false) => {
664 format!("({target_text} < {start_text} or {target_text} > {end_text})")
665 }
666 (false, true) => format!(
667 "{target_text} >= least({start_text}, {end_text}) and {target_text} <= greatest({start_text}, {end_text})"
668 ),
669 (true, true) => format!(
670 "({target_text} < least({start_text}, {end_text}) or {target_text} > greatest({start_text}, {end_text}))"
671 ),
672 };
673
674 actions.push(CodeAction {
675 title: "Rewrite as binary expression".to_owned(),
676 edits: vec![Edit::replace(
677 between_expr.syntax().text_range(),
678 replacement,
679 )],
680 kind: ActionKind::RefactorRewrite,
681 });
682
683 Some(())
684}
685
686fn rewrite_not_equals_operator(
687 actions: &mut Vec<CodeAction>,
688 file: &ast::SourceFile,
689 offset: TextSize,
690) -> Option<()> {
691 let token = token_from_offset(file, offset)?;
692 let bin_expr = token.parent_ancestors().find_map(ast::BinExpr::cast)?;
693
694 let (op_token, replacement, title) = match bin_expr.op()? {
695 ast::BinOp::Neq(token) => (token, "<>", "Rewrite `!=` as `<>`"),
696 ast::BinOp::Neqb(token) => (token, "!=", "Rewrite `<>` as `!=`"),
697 _ => return None,
698 };
699
700 actions.push(CodeAction {
701 title: title.to_owned(),
702 edits: vec![Edit::replace(op_token.text_range(), replacement.to_owned())],
703 kind: ActionKind::RefactorRewrite,
704 });
705
706 Some(())
707}
708
709fn rewrite_timestamp_type(
710 actions: &mut Vec<CodeAction>,
711 file: &ast::SourceFile,
712 offset: TextSize,
713) -> Option<()> {
714 let token = token_from_offset(file, offset)?;
715 let time_type = token.parent_ancestors().find_map(ast::TimeType::cast)?;
716
717 let replacement = match time_type.timezone()? {
718 ast::Timezone::WithoutTimezone(_) => {
719 if time_type.timestamp_token().is_some() {
720 "timestamp"
721 } else {
722 "time"
723 }
724 }
725 ast::Timezone::WithTimezone(_) => {
726 if time_type.timestamp_token().is_some() {
727 "timestamptz"
728 } else {
729 "timetz"
730 }
731 }
732 };
733
734 actions.push(CodeAction {
735 title: format!("Rewrite as `{replacement}`"),
736 edits: vec![Edit::replace(time_type.syntax().text_range(), replacement)],
737 kind: ActionKind::RefactorRewrite,
738 });
739
740 Some(())
741}
742
743fn rewrite_values_as_select(
744 actions: &mut Vec<CodeAction>,
745 file: &ast::SourceFile,
746 offset: TextSize,
747) -> Option<()> {
748 let token = token_from_offset(file, offset)?;
749 let values = token.parent_ancestors().find_map(ast::Values::cast)?;
750
751 let value_token_start = values.values_token().map(|x| x.text_range().start())?;
752 let values_end = values.syntax().text_range().end();
753 let values_range = TextRange::new(value_token_start, values_end);
755
756 let mut rows = values.row_list()?.rows();
757
758 let first_targets: Vec<_> = rows
759 .next()?
760 .exprs()
761 .enumerate()
762 .map(|(idx, expr)| format!("{} as column{}", expr.syntax().text(), idx + 1))
763 .collect();
764
765 if first_targets.is_empty() {
766 return None;
767 }
768
769 let mut select_parts = vec![format!("select {}", first_targets.join(", "))];
770
771 for row in rows {
772 let row_targets = row
773 .exprs()
774 .map(|e| e.syntax().text().to_string())
775 .join(", ");
776 if row_targets.is_empty() {
777 return None;
778 }
779 select_parts.push(format!("union all\nselect {}", row_targets));
780 }
781
782 let select_stmt = select_parts.join("\n");
783
784 actions.push(CodeAction {
785 title: "Rewrite as `select`".to_owned(),
786 edits: vec![Edit::replace(values_range, select_stmt)],
787 kind: ActionKind::RefactorRewrite,
788 });
789
790 Some(())
791}
792
793fn is_values_row_column_name(target: &ast::Target, idx: usize) -> bool {
794 let Some(as_name) = target.as_name() else {
795 return false;
796 };
797 let Some(name) = as_name.name() else {
798 return false;
799 };
800 let expected = format!("column{}", idx + 1);
801 if Name::from_node(&name) != Name::from_string(expected) {
802 return false;
803 }
804 true
805}
806
807enum SelectContext {
808 Compound(ast::CompoundSelect),
809 Single(ast::Select),
810}
811
812impl SelectContext {
813 fn iter(&self) -> Option<Box<dyn Iterator<Item = ast::Select>>> {
814 fn variant_iter(
818 variant: ast::SelectVariant,
819 ) -> Option<Box<dyn Iterator<Item = ast::Select>>> {
820 match variant {
821 ast::SelectVariant::Select(select) => Some(Box::new(iter::once(select))),
822 ast::SelectVariant::CompoundSelect(compound) => compound_iter(&compound),
823 ast::SelectVariant::ParenSelect(_)
824 | ast::SelectVariant::SelectInto(_)
825 | ast::SelectVariant::Table(_)
826 | ast::SelectVariant::Values(_) => None,
827 }
828 }
829
830 fn compound_iter(
831 node: &ast::CompoundSelect,
832 ) -> Option<Box<dyn Iterator<Item = ast::Select>>> {
833 let lhs_iter = node
834 .lhs()
835 .map(variant_iter)
836 .unwrap_or_else(|| Some(Box::new(iter::empty())))?;
837 let rhs_iter = node
838 .rhs()
839 .map(variant_iter)
840 .unwrap_or_else(|| Some(Box::new(iter::empty())))?;
841 Some(Box::new(lhs_iter.chain(rhs_iter)))
842 }
843
844 match self {
845 SelectContext::Compound(compound) => compound_iter(compound),
846 SelectContext::Single(select) => Some(Box::new(iter::once(select.clone()))),
847 }
848 }
849}
850
851fn rewrite_select_as_values(
852 actions: &mut Vec<CodeAction>,
853 file: &ast::SourceFile,
854 offset: TextSize,
855) -> Option<()> {
856 let token = token_from_offset(file, offset)?;
857
858 let parent = find_select_parent(token)?;
859
860 let mut selects = parent.iter()?.peekable();
861 let select_token_start = selects
862 .peek()?
863 .select_clause()
864 .and_then(|x| x.select_token())
865 .map(|x| x.text_range().start())?;
866
867 let mut rows = vec![];
868 for (idx, select) in selects.enumerate() {
869 let exprs: Vec<String> = select
870 .select_clause()?
871 .target_list()?
872 .targets()
873 .enumerate()
874 .map(|(i, t)| {
875 if idx != 0 || is_values_row_column_name(&t, i) {
876 t.expr().map(|expr| expr.syntax().text().to_string())
877 } else {
878 None
879 }
880 })
881 .collect::<Option<_>>()?;
882
883 if exprs.is_empty() {
884 return None;
885 }
886
887 rows.push(format!("({})", exprs.join(", ")));
888 }
889
890 let values_stmt = format!("values {}", rows.join(", "));
891
892 let select_end = match &parent {
893 SelectContext::Compound(compound) => compound.syntax().text_range().end(),
894 SelectContext::Single(select) => select.syntax().text_range().end(),
895 };
896 let select_range = TextRange::new(select_token_start, select_end);
897
898 actions.push(CodeAction {
899 title: "Rewrite as `values`".to_owned(),
900 edits: vec![Edit::replace(select_range, values_stmt)],
901 kind: ActionKind::RefactorRewrite,
902 });
903
904 Some(())
905}
906
907fn find_select_parent(token: SyntaxToken) -> Option<SelectContext> {
908 let mut found_select = None;
909 let mut found_compound = None;
910 for node in token.parent_ancestors() {
911 if let Some(compound_select) = ast::CompoundSelect::cast(node.clone()) {
912 if compound_select.union_token().is_some() && compound_select.all_token().is_some() {
913 found_compound = Some(SelectContext::Compound(compound_select));
914 } else {
915 break;
916 }
917 }
918 if found_select.is_none()
919 && let Some(select) = ast::Select::cast(node)
920 {
921 found_select = Some(SelectContext::Single(select));
922 }
923 }
924 found_compound.or(found_select)
925}
926
927#[cfg(test)]
928mod test {
929 use super::*;
930 use crate::test_utils::fixture;
931 use insta::assert_snapshot;
932 use rowan::TextSize;
933 use squawk_syntax::ast;
934
935 fn apply_code_action(
936 f: impl Fn(&mut Vec<CodeAction>, &ast::SourceFile, TextSize) -> Option<()>,
937 sql: &str,
938 ) -> String {
939 let (mut offset, sql) = fixture(sql);
940 let parse = ast::SourceFile::parse(&sql);
941 let file: ast::SourceFile = parse.tree();
942
943 offset = offset.checked_sub(1.into()).unwrap_or_default();
944
945 let mut actions = vec![];
946 f(&mut actions, &file, offset);
947
948 assert!(
949 !actions.is_empty(),
950 "We should always have actions for `apply_code_action`. If you want to ensure there are no actions, use `code_action_not_applicable` instead."
951 );
952
953 let action = &actions[0];
954
955 match action.kind {
956 ActionKind::QuickFix => {
957 }
959 ActionKind::RefactorRewrite => {
960 assert_eq!(parse.errors(), vec![]);
961 }
962 }
963
964 let mut result = sql.clone();
965
966 let mut edits = action.edits.clone();
967 edits.sort_by_key(|e| e.text_range.start());
968 check_overlap(&edits);
969 edits.reverse();
970
971 for edit in edits {
972 let start: usize = edit.text_range.start().into();
973 let end: usize = edit.text_range.end().into();
974 let replacement = edit.text.as_deref().unwrap_or("");
975 result.replace_range(start..end, replacement);
976 }
977
978 let reparse = ast::SourceFile::parse(&result);
979
980 match action.kind {
981 ActionKind::QuickFix => {
982 }
984 ActionKind::RefactorRewrite => {
985 assert_eq!(
986 reparse.errors(),
987 vec![],
988 "Code actions shouldn't cause syntax errors"
989 );
990 }
991 }
992
993 result
994 }
995
996 fn check_overlap(edits: &[Edit]) {
1001 for (edit_i, edit_j) in edits.iter().zip(edits.iter().skip(1)) {
1002 if let Some(intersection) = edit_i.text_range.intersect(edit_j.text_range) {
1003 assert!(
1004 intersection.is_empty(),
1005 "Edit ranges must not overlap: {:?} and {:?} intersect at {:?}",
1006 edit_i.text_range,
1007 edit_j.text_range,
1008 intersection
1009 );
1010 }
1011 }
1012 }
1013
1014 fn code_action_not_applicable_(
1015 f: impl Fn(&mut Vec<CodeAction>, &ast::SourceFile, TextSize) -> Option<()>,
1016 sql: &str,
1017 allow_errors: bool,
1018 ) -> bool {
1019 let (offset, sql) = fixture(sql);
1020 let parse = ast::SourceFile::parse(&sql);
1021 if !allow_errors {
1022 assert_eq!(parse.errors(), vec![]);
1023 }
1024 let file: ast::SourceFile = parse.tree();
1025
1026 let mut actions = vec![];
1027 f(&mut actions, &file, offset);
1028 actions.is_empty()
1029 }
1030
1031 fn code_action_not_applicable(
1032 f: impl Fn(&mut Vec<CodeAction>, &ast::SourceFile, TextSize) -> Option<()>,
1033 sql: &str,
1034 ) -> bool {
1035 code_action_not_applicable_(f, sql, false)
1036 }
1037
1038 fn code_action_not_applicable_with_errors(
1039 f: impl Fn(&mut Vec<CodeAction>, &ast::SourceFile, TextSize) -> Option<()>,
1040 sql: &str,
1041 ) -> bool {
1042 code_action_not_applicable_(f, sql, true)
1043 }
1044
1045 #[test]
1046 fn remove_else_clause_() {
1047 assert_snapshot!(apply_code_action(
1048 remove_else_clause,
1049 "select case x when true then 1 else$0 2 end;"),
1050 @"select case x when true then 1 end;"
1051 );
1052 }
1053
1054 #[test]
1055 fn remove_else_clause_before_token() {
1056 assert_snapshot!(apply_code_action(
1057 remove_else_clause,
1058 "select case x when true then 1 e$0lse 2 end;"),
1059 @"select case x when true then 1 end;"
1060 );
1061 }
1062
1063 #[test]
1064 fn remove_else_clause_not_applicable() {
1065 assert!(code_action_not_applicable(
1066 remove_else_clause,
1067 "select case x when true then 1 else 2 end$0;"
1068 ));
1069 }
1070
1071 #[test]
1072 fn rewrite_string() {
1073 assert_snapshot!(apply_code_action(
1074 rewrite_as_dollar_quoted_string,
1075 "select 'fo$0o';"),
1076 @"select $$foo$$;"
1077 );
1078 }
1079
1080 #[test]
1081 fn rewrite_string_with_single_quote() {
1082 assert_snapshot!(apply_code_action(
1083 rewrite_as_dollar_quoted_string,
1084 "select 'it''s$0 nice';"),
1085 @"select $$it's nice$$;"
1086 );
1087 }
1088
1089 #[test]
1090 fn rewrite_string_with_dollar_signs() {
1091 assert_snapshot!(apply_code_action(
1092 rewrite_as_dollar_quoted_string,
1093 "select 'foo $$ ba$0r';"),
1094 @"select $q$foo $$ bar$q$;"
1095 );
1096 }
1097
1098 #[test]
1099 fn rewrite_string_when_trailing_dollar() {
1100 assert_snapshot!(apply_code_action(
1101 rewrite_as_dollar_quoted_string,
1102 "select 'foo $'$0;"),
1103 @"select $q$foo $$q$;"
1104 );
1105 }
1106
1107 #[test]
1108 fn rewrite_string_not_applicable() {
1109 assert!(code_action_not_applicable(
1110 rewrite_as_dollar_quoted_string,
1111 "select 1 + $0 2;"
1112 ));
1113 }
1114
1115 #[test]
1116 fn rewrite_prefix_string_not_applicable() {
1117 assert!(code_action_not_applicable(
1118 rewrite_as_dollar_quoted_string,
1119 "select b'foo$0';"
1120 ));
1121 }
1122
1123 #[test]
1124 fn rewrite_dollar_string() {
1125 assert_snapshot!(apply_code_action(
1126 rewrite_as_regular_string,
1127 "select $$fo$0o$$;"),
1128 @"select 'foo';"
1129 );
1130 }
1131
1132 #[test]
1133 fn rewrite_dollar_string_with_tag() {
1134 assert_snapshot!(apply_code_action(
1135 rewrite_as_regular_string,
1136 "select $tag$fo$0o$tag$;"),
1137 @"select 'foo';"
1138 );
1139 }
1140
1141 #[test]
1142 fn rewrite_dollar_string_with_quote() {
1143 assert_snapshot!(apply_code_action(
1144 rewrite_as_regular_string,
1145 "select $$it'$0s fine$$;"),
1146 @"select 'it''s fine';"
1147 );
1148 }
1149
1150 #[test]
1151 fn rewrite_dollar_string_not_applicable() {
1152 assert!(code_action_not_applicable(
1153 rewrite_as_regular_string,
1154 "select 'foo$0';"
1155 ));
1156 }
1157
1158 #[test]
1159 fn rewrite_table_as_select_simple() {
1160 assert_snapshot!(apply_code_action(
1161 rewrite_table_as_select,
1162 "tab$0le foo;"),
1163 @"select * from foo;"
1164 );
1165 }
1166
1167 #[test]
1168 fn rewrite_table_as_select_qualified() {
1169 assert_snapshot!(apply_code_action(
1170 rewrite_table_as_select,
1171 "ta$0ble schema.foo;"),
1172 @"select * from schema.foo;"
1173 );
1174 }
1175
1176 #[test]
1177 fn rewrite_table_as_select_after_keyword() {
1178 assert_snapshot!(apply_code_action(
1179 rewrite_table_as_select,
1180 "table$0 bar;"),
1181 @"select * from bar;"
1182 );
1183 }
1184
1185 #[test]
1186 fn rewrite_table_as_select_on_table_name() {
1187 assert_snapshot!(apply_code_action(
1188 rewrite_table_as_select,
1189 "table fo$0o;"),
1190 @"select * from foo;"
1191 );
1192 }
1193
1194 #[test]
1195 fn rewrite_table_as_select_not_applicable() {
1196 assert!(code_action_not_applicable(
1197 rewrite_table_as_select,
1198 "select * from foo$0;"
1199 ));
1200 }
1201
1202 #[test]
1203 fn rewrite_select_as_table_simple() {
1204 assert_snapshot!(apply_code_action(
1205 rewrite_select_as_table,
1206 "sel$0ect * from foo;"),
1207 @"table foo;"
1208 );
1209 }
1210
1211 #[test]
1212 fn rewrite_select_as_table_qualified() {
1213 assert_snapshot!(apply_code_action(
1214 rewrite_select_as_table,
1215 "select * from sch$0ema.foo;"),
1216 @"table schema.foo;"
1217 );
1218 }
1219
1220 #[test]
1221 fn rewrite_select_as_table_on_star() {
1222 assert_snapshot!(apply_code_action(
1223 rewrite_select_as_table,
1224 "select $0* from bar;"),
1225 @"table bar;"
1226 );
1227 }
1228
1229 #[test]
1230 fn rewrite_select_as_table_on_from() {
1231 assert_snapshot!(apply_code_action(
1232 rewrite_select_as_table,
1233 "select * fr$0om baz;"),
1234 @"table baz;"
1235 );
1236 }
1237
1238 #[test]
1239 fn rewrite_select_as_table_not_applicable_with_where() {
1240 assert!(code_action_not_applicable(
1241 rewrite_select_as_table,
1242 "select * from foo$0 where x = 1;"
1243 ));
1244 }
1245
1246 #[test]
1247 fn rewrite_select_as_table_not_applicable_with_order_by() {
1248 assert!(code_action_not_applicable(
1249 rewrite_select_as_table,
1250 "select * from foo$0 order by x;"
1251 ));
1252 }
1253
1254 #[test]
1255 fn rewrite_select_as_table_not_applicable_with_limit() {
1256 assert!(code_action_not_applicable(
1257 rewrite_select_as_table,
1258 "select * from foo$0 limit 10;"
1259 ));
1260 }
1261
1262 #[test]
1263 fn add_schema_simple() {
1264 assert_snapshot!(apply_code_action(
1265 add_schema,
1266 "create table t$0(a text, b int);"),
1267 @"create table public.t(a text, b int);"
1268 );
1269 }
1270
1271 #[test]
1272 fn add_schema_create_foreign_table() {
1273 assert_snapshot!(apply_code_action(
1274 add_schema,
1275 "create foreign table t$0(a text, b int) server foo;"),
1276 @"create foreign table public.t(a text, b int) server foo;"
1277 );
1278 }
1279
1280 #[test]
1281 fn add_schema_create_function() {
1282 assert_snapshot!(apply_code_action(
1283 add_schema,
1284 "create function f$0() returns int8\n as 'select 1'\n language sql;"),
1285 @"create function public.f() returns int8
1286 as 'select 1'
1287 language sql;"
1288 );
1289 }
1290
1291 #[test]
1292 fn add_schema_create_type() {
1293 assert_snapshot!(apply_code_action(
1294 add_schema,
1295 "create type t$0 as enum ();"),
1296 @"create type public.t as enum ();"
1297 );
1298 }
1299
1300 #[test]
1301 fn add_schema_table_stmt() {
1302 assert_snapshot!(apply_code_action(
1303 add_schema,
1304 "table t$0;"),
1305 @"table public.t;"
1306 );
1307 }
1308
1309 #[test]
1310 fn add_schema_select_from() {
1311 assert_snapshot!(apply_code_action(
1312 add_schema,
1313 "create table t(a text, b int);
1314 select t from t$0;"),
1315 @"create table t(a text, b int);
1316 select t from public.t;"
1317 );
1318 }
1319
1320 #[test]
1321 fn add_schema_select_table_value() {
1322 assert!(code_action_not_applicable(
1325 add_schema,
1326 "create table t(a text, b int);
1327 select t$0 from t;"
1328 ));
1329 }
1330
1331 #[test]
1332 fn add_schema_select_unqualified_column() {
1333 assert!(code_action_not_applicable(
1336 add_schema,
1337 "create table t(a text, b int);
1338 select a$0 from t;"
1339 ));
1340 }
1341
1342 #[test]
1343 fn add_schema_select_qualified_column() {
1344 assert!(code_action_not_applicable(
1347 add_schema,
1348 "create table t(c text);
1349 select t$0.c from t;"
1350 ));
1351 }
1352
1353 #[test]
1354 fn add_schema_with_search_path() {
1355 assert_snapshot!(
1356 apply_code_action(
1357 add_schema,
1358 "
1359set search_path to myschema;
1360create table t$0(a text, b int);"
1361 ),
1362 @"
1363set search_path to myschema;
1364create table myschema.t(a text, b int);"
1365 );
1366 }
1367
1368 #[test]
1369 fn add_schema_not_applicable_with_schema() {
1370 assert!(code_action_not_applicable(
1371 add_schema,
1372 "create table myschema.t$0(a text, b int);"
1373 ));
1374 }
1375
1376 #[test]
1377 fn add_schema_function_call() {
1378 assert_snapshot!(apply_code_action(
1379 add_schema,
1380 "
1381create function f() returns int8
1382 as 'select 1'
1383 language sql;
1384
1385select f$0();"),
1386 @"
1387create function f() returns int8
1388 as 'select 1'
1389 language sql;
1390
1391select public.f();"
1392 );
1393 }
1394
1395 #[test]
1396 fn add_schema_function_call_not_applicable_with_schema() {
1397 assert!(code_action_not_applicable(
1398 add_schema,
1399 "
1400create function f() returns int8 as 'select 1' language sql;
1401select myschema.f$0();"
1402 ));
1403 }
1404
1405 #[test]
1406 fn rewrite_select_as_table_not_applicable_with_distinct() {
1407 assert!(code_action_not_applicable(
1408 rewrite_select_as_table,
1409 "select distinct * from foo$0;"
1410 ));
1411 }
1412
1413 #[test]
1414 fn rewrite_select_as_table_not_applicable_with_columns() {
1415 assert!(code_action_not_applicable(
1416 rewrite_select_as_table,
1417 "select id, name from foo$0;"
1418 ));
1419 }
1420
1421 #[test]
1422 fn rewrite_select_as_table_not_applicable_with_join() {
1423 assert!(code_action_not_applicable(
1424 rewrite_select_as_table,
1425 "select * from foo$0 join bar on foo.id = bar.id;"
1426 ));
1427 }
1428
1429 #[test]
1430 fn rewrite_select_as_table_not_applicable_with_alias() {
1431 assert!(code_action_not_applicable(
1432 rewrite_select_as_table,
1433 "select * from foo$0 f;"
1434 ));
1435 }
1436
1437 #[test]
1438 fn rewrite_select_as_table_not_applicable_with_multiple_tables() {
1439 assert!(code_action_not_applicable(
1440 rewrite_select_as_table,
1441 "select * from foo$0, bar;"
1442 ));
1443 }
1444
1445 #[test]
1446 fn rewrite_select_as_table_not_applicable_on_table() {
1447 assert!(code_action_not_applicable(
1448 rewrite_select_as_table,
1449 "table foo$0;"
1450 ));
1451 }
1452
1453 #[test]
1454 fn quote_identifier_on_name_ref() {
1455 assert_snapshot!(apply_code_action(
1456 quote_identifier,
1457 "select x$0 from t;"),
1458 @r#"select "x" from t;"#
1459 );
1460 }
1461
1462 #[test]
1463 fn quote_identifier_on_name() {
1464 assert_snapshot!(apply_code_action(
1465 quote_identifier,
1466 "create table T(X$0 int);"),
1467 @r#"create table T("x" int);"#
1468 );
1469 }
1470
1471 #[test]
1472 fn quote_identifier_lowercases() {
1473 assert_snapshot!(apply_code_action(
1474 quote_identifier,
1475 "create table T(COL$0 int);"),
1476 @r#"create table T("col" int);"#
1477 );
1478 }
1479
1480 #[test]
1481 fn quote_identifier_not_applicable_when_already_quoted() {
1482 assert!(code_action_not_applicable(
1483 quote_identifier,
1484 r#"select "x"$0 from t;"#
1485 ));
1486 }
1487
1488 #[test]
1489 fn quote_identifier_not_applicable_on_select_keyword() {
1490 assert!(code_action_not_applicable(
1491 quote_identifier,
1492 "sel$0ect x from t;"
1493 ));
1494 }
1495
1496 #[test]
1497 fn quote_identifier_on_keyword_column_name() {
1498 assert_snapshot!(apply_code_action(
1499 quote_identifier,
1500 "select te$0xt from t;"),
1501 @r#"select "text" from t;"#
1502 );
1503 }
1504
1505 #[test]
1506 fn quote_identifier_example_select() {
1507 assert_snapshot!(apply_code_action(
1508 quote_identifier,
1509 "select x$0 from t;"),
1510 @r#"select "x" from t;"#
1511 );
1512 }
1513
1514 #[test]
1515 fn quote_identifier_example_create_table() {
1516 assert_snapshot!(apply_code_action(
1517 quote_identifier,
1518 "create table T(X$0 int);"),
1519 @r#"create table T("x" int);"#
1520 );
1521 }
1522
1523 #[test]
1524 fn unquote_identifier_simple() {
1525 assert_snapshot!(apply_code_action(
1526 unquote_identifier,
1527 r#"select "x"$0 from t;"#),
1528 @"select x from t;"
1529 );
1530 }
1531
1532 #[test]
1533 fn unquote_identifier_with_underscore() {
1534 assert_snapshot!(apply_code_action(
1535 unquote_identifier,
1536 r#"select "user_id"$0 from t;"#),
1537 @"select user_id from t;"
1538 );
1539 }
1540
1541 #[test]
1542 fn unquote_identifier_with_digits() {
1543 assert_snapshot!(apply_code_action(
1544 unquote_identifier,
1545 r#"select "x123"$0 from t;"#),
1546 @"select x123 from t;"
1547 );
1548 }
1549
1550 #[test]
1551 fn unquote_identifier_with_dollar() {
1552 assert_snapshot!(apply_code_action(
1553 unquote_identifier,
1554 r#"select "my_table$1"$0 from t;"#),
1555 @"select my_table$1 from t;"
1556 );
1557 }
1558
1559 #[test]
1560 fn unquote_identifier_starts_with_underscore() {
1561 assert_snapshot!(apply_code_action(
1562 unquote_identifier,
1563 r#"select "_col"$0 from t;"#),
1564 @"select _col from t;"
1565 );
1566 }
1567
1568 #[test]
1569 fn unquote_identifier_starts_with_unicode() {
1570 assert_snapshot!(apply_code_action(
1571 unquote_identifier,
1572 r#"select "é"$0 from t;"#),
1573 @"select é from t;"
1574 );
1575 }
1576
1577 #[test]
1578 fn unquote_identifier_not_applicable() {
1579 assert!(code_action_not_applicable(
1581 unquote_identifier,
1582 r#"select "X"$0 from t;"#
1583 ));
1584 assert!(code_action_not_applicable(
1586 unquote_identifier,
1587 r#"select "Foo"$0 from t;"#
1588 ));
1589 assert!(code_action_not_applicable(
1591 unquote_identifier,
1592 r#"select "my-col"$0 from t;"#
1593 ));
1594 assert!(code_action_not_applicable(
1596 unquote_identifier,
1597 r#"select "123"$0 from t;"#
1598 ));
1599 assert!(code_action_not_applicable(
1601 unquote_identifier,
1602 r#"select "foo bar"$0 from t;"#
1603 ));
1604 assert!(code_action_not_applicable(
1606 unquote_identifier,
1607 r#"select "foo""bar"$0 from t;"#
1608 ));
1609 assert!(code_action_not_applicable(
1611 unquote_identifier,
1612 "select x$0 from t;"
1613 ));
1614 assert!(code_action_not_applicable(
1616 unquote_identifier,
1617 r#"select "my[col]"$0 from t;"#
1618 ));
1619 assert!(code_action_not_applicable(
1621 unquote_identifier,
1622 r#"select "my{}"$0 from t;"#
1623 ));
1624 assert!(code_action_not_applicable(
1626 unquote_identifier,
1627 r#"select "select"$0 from t;"#
1628 ));
1629 }
1630
1631 #[test]
1632 fn unquote_identifier_on_name() {
1633 assert_snapshot!(apply_code_action(
1634 unquote_identifier,
1635 r#"create table T("x"$0 int);"#),
1636 @"create table T(x int);"
1637 );
1638 }
1639
1640 #[test]
1641 fn add_explicit_alias_simple_column() {
1642 assert_snapshot!(apply_code_action(
1643 add_explicit_alias,
1644 "select col_na$0me from t;"),
1645 @"select col_name as col_name from t;"
1646 );
1647 }
1648
1649 #[test]
1650 fn add_explicit_alias_quoted_identifier() {
1651 assert_snapshot!(apply_code_action(
1652 add_explicit_alias,
1653 r#"select "b"$0 from t;"#),
1654 @r#"select "b" as b from t;"#
1655 );
1656 }
1657
1658 #[test]
1659 fn add_explicit_alias_field_expr() {
1660 assert_snapshot!(apply_code_action(
1661 add_explicit_alias,
1662 "select t.col$0umn from t;"),
1663 @"select t.column as column from t;"
1664 );
1665 }
1666
1667 #[test]
1668 fn add_explicit_alias_function_call() {
1669 assert_snapshot!(apply_code_action(
1670 add_explicit_alias,
1671 "select cou$0nt(*) from t;"),
1672 @"select count(*) as count from t;"
1673 );
1674 }
1675
1676 #[test]
1677 fn add_explicit_alias_cast_to_type() {
1678 assert_snapshot!(apply_code_action(
1679 add_explicit_alias,
1680 "select '1'::bigi$0nt from t;"),
1681 @"select '1'::bigint as int8 from t;"
1682 );
1683 }
1684
1685 #[test]
1686 fn add_explicit_alias_cast_column() {
1687 assert_snapshot!(apply_code_action(
1688 add_explicit_alias,
1689 "select col_na$0me::text from t;"),
1690 @"select col_name::text as col_name from t;"
1691 );
1692 }
1693
1694 #[test]
1695 fn add_explicit_alias_case_expr() {
1696 assert_snapshot!(apply_code_action(
1697 add_explicit_alias,
1698 "select ca$0se when true then 'a' end from t;"),
1699 @"select case when true then 'a' end as case from t;"
1700 );
1701 }
1702
1703 #[test]
1704 fn add_explicit_alias_case_with_else() {
1705 assert_snapshot!(apply_code_action(
1706 add_explicit_alias,
1707 "select ca$0se when true then 'a' else now()::text end from t;"),
1708 @"select case when true then 'a' else now()::text end as now from t;"
1709 );
1710 }
1711
1712 #[test]
1713 fn add_explicit_alias_array() {
1714 assert_snapshot!(apply_code_action(
1715 add_explicit_alias,
1716 "select arr$0ay[1, 2, 3] from t;"),
1717 @"select array[1, 2, 3] as array from t;"
1718 );
1719 }
1720
1721 #[test]
1722 fn add_explicit_alias_not_applicable_already_has_alias() {
1723 assert!(code_action_not_applicable(
1724 add_explicit_alias,
1725 "select col_name$0 as foo from t;"
1726 ));
1727 }
1728
1729 #[test]
1730 fn add_explicit_alias_unknown_column() {
1731 assert_snapshot!(apply_code_action(
1732 add_explicit_alias,
1733 "select 1 $0+ 2 from t;"),
1734 @r#"select 1 + 2 as "?column?" from t;"#
1735 );
1736 }
1737
1738 #[test]
1739 fn add_explicit_alias_not_applicable_star() {
1740 assert!(code_action_not_applicable(
1741 add_explicit_alias,
1742 "select $0* from t;"
1743 ));
1744 }
1745
1746 #[test]
1747 fn add_explicit_alias_not_applicable_qualified_star() {
1748 assert!(code_action_not_applicable(
1749 add_explicit_alias,
1750 "with t as (select 1 a) select t.*$0 from t;"
1751 ));
1752 }
1753
1754 #[test]
1755 fn add_explicit_alias_literal() {
1756 assert_snapshot!(apply_code_action(
1757 add_explicit_alias,
1758 "select 'foo$0' from t;"),
1759 @r#"select 'foo' as "?column?" from t;"#
1760 );
1761 }
1762
1763 #[test]
1764 fn remove_redundant_alias_simple() {
1765 assert_snapshot!(apply_code_action(
1766 remove_redundant_alias,
1767 "select col_name as col_na$0me from t;"),
1768 @"select col_name from t;"
1769 );
1770 }
1771
1772 #[test]
1773 fn remove_redundant_alias_quoted() {
1774 assert_snapshot!(apply_code_action(
1775 remove_redundant_alias,
1776 r#"select "x"$0 as x from t;"#),
1777 @r#"select "x" from t;"#
1778 );
1779 }
1780
1781 #[test]
1782 fn remove_redundant_alias_case_insensitive() {
1783 assert_snapshot!(apply_code_action(
1784 remove_redundant_alias,
1785 "select col_name$0 as COL_NAME from t;"),
1786 @"select col_name from t;"
1787 );
1788 }
1789
1790 #[test]
1791 fn remove_redundant_alias_function() {
1792 assert_snapshot!(apply_code_action(
1793 remove_redundant_alias,
1794 "select count(*)$0 as count from t;"),
1795 @"select count(*) from t;"
1796 );
1797 }
1798
1799 #[test]
1800 fn remove_redundant_alias_field_expr() {
1801 assert_snapshot!(apply_code_action(
1802 remove_redundant_alias,
1803 "select t.col$0umn as column from t;"),
1804 @"select t.column from t;"
1805 );
1806 }
1807
1808 #[test]
1809 fn remove_redundant_alias_not_applicable_different_name() {
1810 assert!(code_action_not_applicable(
1811 remove_redundant_alias,
1812 "select col_name$0 as foo from t;"
1813 ));
1814 }
1815
1816 #[test]
1817 fn remove_redundant_alias_not_applicable_no_alias() {
1818 assert!(code_action_not_applicable(
1819 remove_redundant_alias,
1820 "select col_name$0 from t;"
1821 ));
1822 }
1823
1824 #[test]
1825 fn rewrite_cast_to_double_colon_simple() {
1826 assert_snapshot!(apply_code_action(
1827 rewrite_cast_to_double_colon,
1828 "select ca$0st(foo as text) from t;"),
1829 @"select foo::text from t;"
1830 );
1831 }
1832
1833 #[test]
1834 fn rewrite_cast_to_double_colon_on_column() {
1835 assert_snapshot!(apply_code_action(
1836 rewrite_cast_to_double_colon,
1837 "select cast(col_na$0me as int) from t;"),
1838 @"select col_name::int from t;"
1839 );
1840 }
1841
1842 #[test]
1843 fn rewrite_cast_to_double_colon_on_type() {
1844 assert_snapshot!(apply_code_action(
1845 rewrite_cast_to_double_colon,
1846 "select cast(x as bigi$0nt) from t;"),
1847 @"select x::bigint from t;"
1848 );
1849 }
1850
1851 #[test]
1852 fn rewrite_cast_to_double_colon_qualified_type() {
1853 assert_snapshot!(apply_code_action(
1854 rewrite_cast_to_double_colon,
1855 "select cast(x as pg_cata$0log.text) from t;"),
1856 @"select x::pg_catalog.text from t;"
1857 );
1858 }
1859
1860 #[test]
1861 fn rewrite_cast_to_double_colon_expression() {
1862 assert_snapshot!(apply_code_action(
1863 rewrite_cast_to_double_colon,
1864 "select ca$0st(1 + 2 as bigint) from t;"),
1865 @"select 1 + 2::bigint from t;"
1866 );
1867 }
1868
1869 #[test]
1870 fn rewrite_cast_to_double_colon_type_first_syntax() {
1871 assert_snapshot!(apply_code_action(
1872 rewrite_cast_to_double_colon,
1873 "select in$0t '1';"),
1874 @"select '1'::int;"
1875 );
1876 }
1877
1878 #[test]
1879 fn rewrite_cast_to_double_colon_type_first_qualified() {
1880 assert_snapshot!(apply_code_action(
1881 rewrite_cast_to_double_colon,
1882 "select pg_catalog.int$04 '1';"),
1883 @"select '1'::pg_catalog.int4;"
1884 );
1885 }
1886
1887 #[test]
1888 fn rewrite_cast_to_double_colon_not_applicable_already_double_colon() {
1889 assert!(code_action_not_applicable(
1890 rewrite_cast_to_double_colon,
1891 "select foo::te$0xt from t;"
1892 ));
1893 }
1894
1895 #[test]
1896 fn rewrite_cast_to_double_colon_not_applicable_outside_cast() {
1897 assert!(code_action_not_applicable(
1898 rewrite_cast_to_double_colon,
1899 "select fo$0o from t;"
1900 ));
1901 }
1902
1903 #[test]
1904 fn rewrite_double_colon_to_cast_simple() {
1905 assert_snapshot!(apply_code_action(
1906 rewrite_double_colon_to_cast,
1907 "select foo::te$0xt from t;"),
1908 @"select cast(foo as text) from t;"
1909 );
1910 }
1911
1912 #[test]
1913 fn rewrite_double_colon_to_cast_on_column() {
1914 assert_snapshot!(apply_code_action(
1915 rewrite_double_colon_to_cast,
1916 "select col_na$0me::int from t;"),
1917 @"select cast(col_name as int) from t;"
1918 );
1919 }
1920
1921 #[test]
1922 fn rewrite_double_colon_to_cast_on_type() {
1923 assert_snapshot!(apply_code_action(
1924 rewrite_double_colon_to_cast,
1925 "select x::bigi$0nt from t;"),
1926 @"select cast(x as bigint) from t;"
1927 );
1928 }
1929
1930 #[test]
1931 fn rewrite_double_colon_to_cast_qualified_type() {
1932 assert_snapshot!(apply_code_action(
1933 rewrite_double_colon_to_cast,
1934 "select x::pg_cata$0log.text from t;"),
1935 @"select cast(x as pg_catalog.text) from t;"
1936 );
1937 }
1938
1939 #[test]
1940 fn rewrite_double_colon_to_cast_expression() {
1941 assert_snapshot!(apply_code_action(
1942 rewrite_double_colon_to_cast,
1943 "select 1 + 2::bigi$0nt from t;"),
1944 @"select 1 + cast(2 as bigint) from t;"
1945 );
1946 }
1947
1948 #[test]
1949 fn rewrite_type_literal_syntax_to_cast() {
1950 assert_snapshot!(apply_code_action(
1951 rewrite_double_colon_to_cast,
1952 "select in$0t '1';"),
1953 @"select cast('1' as int);"
1954 );
1955 }
1956
1957 #[test]
1958 fn rewrite_qualified_type_literal_syntax_to_cast() {
1959 assert_snapshot!(apply_code_action(
1960 rewrite_double_colon_to_cast,
1961 "select pg_catalog.int$04 '1';"),
1962 @"select cast('1' as pg_catalog.int4);"
1963 );
1964 }
1965
1966 #[test]
1967 fn rewrite_double_colon_to_cast_not_applicable_already_cast() {
1968 assert!(code_action_not_applicable(
1969 rewrite_double_colon_to_cast,
1970 "select ca$0st(foo as text) from t;"
1971 ));
1972 }
1973
1974 #[test]
1975 fn rewrite_double_colon_to_cast_not_applicable_outside_cast() {
1976 assert!(code_action_not_applicable(
1977 rewrite_double_colon_to_cast,
1978 "select fo$0o from t;"
1979 ));
1980 }
1981
1982 #[test]
1983 fn rewrite_between_as_binary_expression_simple() {
1984 assert_snapshot!(apply_code_action(
1985 rewrite_between_as_binary_expression,
1986 "select 2 betw$0een 1 and 3;"
1987 ),
1988 @"select 2 >= 1 and 2 <= 3;"
1989 );
1990 }
1991
1992 #[test]
1993 fn rewrite_not_between_as_binary_expression() {
1994 assert_snapshot!(apply_code_action(
1995 rewrite_between_as_binary_expression,
1996 "select 2 no$0t between 1 and 3;"
1997 ),
1998 @"select (2 < 1 or 2 > 3);"
1999 );
2000 }
2001
2002 #[test]
2003 fn rewrite_between_symmetric_as_binary_expression() {
2004 assert_snapshot!(apply_code_action(
2005 rewrite_between_as_binary_expression,
2006 "select 2 between symme$0tric 3 and 1;"
2007 ),
2008 @"select 2 >= least(3, 1) and 2 <= greatest(3, 1);"
2009 );
2010 }
2011
2012 #[test]
2013 fn rewrite_not_between_symmetric_as_binary_expression() {
2014 assert_snapshot!(apply_code_action(
2015 rewrite_between_as_binary_expression,
2016 "select 2 not between symme$0tric 3 and 1;"
2017 ),
2018 @"select (2 < least(3, 1) or 2 > greatest(3, 1));"
2019 );
2020 }
2021
2022 #[test]
2023 fn rewrite_between_as_binary_expression_not_applicable() {
2024 assert!(code_action_not_applicable(
2025 rewrite_between_as_binary_expression,
2026 "select 1 +$0 2;"
2027 ));
2028 }
2029
2030 #[test]
2031 fn rewrite_not_equals_bang_to_angle() {
2032 assert_snapshot!(
2033 apply_code_action(rewrite_not_equals_operator, "select 1 !$0= 2;"),
2034 @"select 1 <> 2;"
2035 );
2036 }
2037
2038 #[test]
2039 fn rewrite_not_equals_angle_to_bang() {
2040 assert_snapshot!(
2041 apply_code_action(rewrite_not_equals_operator, "select 1 <$0> 2;"),
2042 @"select 1 != 2;"
2043 );
2044 }
2045
2046 #[test]
2047 fn rewrite_not_equals_cursor_on_operand() {
2048 assert_snapshot!(
2049 apply_code_action(rewrite_not_equals_operator, "select a$0 != b from t;"),
2050 @"select a <> b from t;"
2051 );
2052 }
2053
2054 #[test]
2055 fn rewrite_not_equals_not_applicable_other_op() {
2056 assert!(code_action_not_applicable(
2057 rewrite_not_equals_operator,
2058 "select 1 =$0 2;"
2059 ));
2060 }
2061
2062 #[test]
2063 fn rewrite_values_as_select_simple() {
2064 assert_snapshot!(
2065 apply_code_action(rewrite_values_as_select, "valu$0es (1, 'one'), (2, 'two');"),
2066 @r"
2067 select 1 as column1, 'one' as column2
2068 union all
2069 select 2, 'two';
2070 "
2071 );
2072 }
2073
2074 #[test]
2075 fn rewrite_values_as_select_single_row() {
2076 assert_snapshot!(
2077 apply_code_action(rewrite_values_as_select, "val$0ues (1, 2, 3);"),
2078 @"select 1 as column1, 2 as column2, 3 as column3;"
2079 );
2080 }
2081
2082 #[test]
2083 fn rewrite_values_as_select_single_column() {
2084 assert_snapshot!(
2085 apply_code_action(rewrite_values_as_select, "values$0 (1);"),
2086 @"select 1 as column1;"
2087 );
2088 }
2089
2090 #[test]
2091 fn rewrite_values_as_select_multiple_rows() {
2092 assert_snapshot!(
2093 apply_code_action(rewrite_values_as_select, "values (1, 2), (3, 4), (5, 6$0);"),
2094 @r"
2095 select 1 as column1, 2 as column2
2096 union all
2097 select 3, 4
2098 union all
2099 select 5, 6;
2100 "
2101 );
2102 }
2103
2104 #[test]
2105 fn rewrite_values_as_select_with_clause() {
2106 assert_snapshot!(
2107 apply_code_action(
2108 rewrite_values_as_select,
2109 "with cte as (select 1) val$0ues (1, 'one'), (2, 'two');"
2110 ),
2111 @r"
2112 with cte as (select 1) select 1 as column1, 'one' as column2
2113 union all
2114 select 2, 'two';
2115 "
2116 );
2117 }
2118
2119 #[test]
2120 fn rewrite_values_as_select_complex_expressions() {
2121 assert_snapshot!(
2122 apply_code_action(
2123 rewrite_values_as_select,
2124 "values (1 + 2, 'test'::text$0, array[1,2]);"
2125 ),
2126 @"select 1 + 2 as column1, 'test'::text as column2, array[1,2] as column3;"
2127 );
2128 }
2129
2130 #[test]
2131 fn rewrite_values_as_select_on_values_keyword() {
2132 assert_snapshot!(
2133 apply_code_action(rewrite_values_as_select, "val$0ues (1, 2);"),
2134 @"select 1 as column1, 2 as column2;"
2135 );
2136 }
2137
2138 #[test]
2139 fn rewrite_values_as_select_on_row_content() {
2140 assert_snapshot!(
2141 apply_code_action(rewrite_values_as_select, "values (1$0, 2), (3, 4);"),
2142 @r"
2143 select 1 as column1, 2 as column2
2144 union all
2145 select 3, 4;
2146 "
2147 );
2148 }
2149
2150 #[test]
2151 fn rewrite_values_as_select_not_applicable_on_select() {
2152 assert!(code_action_not_applicable(
2153 rewrite_values_as_select,
2154 "sel$0ect 1;"
2155 ));
2156 }
2157
2158 #[test]
2159 fn rewrite_select_as_values_simple() {
2160 assert_snapshot!(
2161 apply_code_action(
2162 rewrite_select_as_values,
2163 "select 1 as column1, 'one' as column2 union all$0 select 2, 'two';"
2164 ),
2165 @"values (1, 'one'), (2, 'two');"
2166 );
2167 }
2168
2169 #[test]
2170 fn rewrite_select_as_values_multiple_rows() {
2171 assert_snapshot!(
2172 apply_code_action(
2173 rewrite_select_as_values,
2174 "select 1 as column1, 2 as column2 union$0 all select 3, 4 union all select 5, 6;"
2175 ),
2176 @"values (1, 2), (3, 4), (5, 6);"
2177 );
2178 }
2179
2180 #[test]
2181 fn rewrite_select_as_values_multiple_rows_cursor_on_second_union() {
2182 assert_snapshot!(
2183 apply_code_action(
2184 rewrite_select_as_values,
2185 "select 1 as column1, 2 as column2 union all select 3, 4 union$0 all select 5, 6;"
2186 ),
2187 @"values (1, 2), (3, 4), (5, 6);"
2188 );
2189 }
2190
2191 #[test]
2192 fn rewrite_select_as_values_single_column() {
2193 assert_snapshot!(
2194 apply_code_action(
2195 rewrite_select_as_values,
2196 "select 1 as column1$0 union all select 2;"
2197 ),
2198 @"values (1), (2);"
2199 );
2200 }
2201
2202 #[test]
2203 fn rewrite_select_as_values_with_clause() {
2204 assert_snapshot!(
2205 apply_code_action(
2206 rewrite_select_as_values,
2207 "with cte as (select 1) select 1 as column1, 'one' as column2 uni$0on all select 2, 'two';"
2208 ),
2209 @"with cte as (select 1) values (1, 'one'), (2, 'two');"
2210 );
2211 }
2212
2213 #[test]
2214 fn rewrite_select_as_values_complex_expressions() {
2215 assert_snapshot!(
2216 apply_code_action(
2217 rewrite_select_as_values,
2218 "select 1 + 2 as column1, 'test'::text as column2$0 union all select 3 * 4, array[1,2]::text;"
2219 ),
2220 @"values (1 + 2, 'test'::text), (3 * 4, array[1,2]::text);"
2221 );
2222 }
2223
2224 #[test]
2225 fn rewrite_select_as_values_single_select() {
2226 assert_snapshot!(
2227 apply_code_action(
2228 rewrite_select_as_values,
2229 "select 1 as column1, 2 as column2$0;"
2230 ),
2231 @"values (1, 2);"
2232 );
2233 }
2234
2235 #[test]
2236 fn rewrite_select_as_values_single_select_with_clause() {
2237 assert_snapshot!(
2238 apply_code_action(
2239 rewrite_select_as_values,
2240 "with cte as (select 1) select 1 as column1$0, 'test' as column2;"
2241 ),
2242 @"with cte as (select 1) values (1, 'test');"
2243 );
2244 }
2245
2246 #[test]
2247 fn rewrite_select_as_values_not_applicable_union_without_all() {
2248 assert!(code_action_not_applicable(
2249 rewrite_select_as_values,
2250 "select 1 as column1 union$0 select 2;"
2251 ));
2252 }
2253
2254 #[test]
2255 fn rewrite_select_as_values_not_applicable_wrong_column_names() {
2256 assert!(code_action_not_applicable(
2257 rewrite_select_as_values,
2258 "select 1 as foo, 2 as bar union all$0 select 3, 4;"
2259 ));
2260 }
2261
2262 #[test]
2263 fn rewrite_select_as_values_not_applicable_missing_aliases() {
2264 assert!(code_action_not_applicable(
2265 rewrite_select_as_values,
2266 "select 1, 2 union all$0 select 3, 4;"
2267 ));
2268 }
2269
2270 #[test]
2271 fn rewrite_select_as_values_case_insensitive_column_names() {
2272 assert_snapshot!(
2273 apply_code_action(
2274 rewrite_select_as_values,
2275 "select 1 as COLUMN1, 2 as CoLuMn2 union all$0 select 3, 4;"
2276 ),
2277 @"values (1, 2), (3, 4);"
2278 );
2279 }
2280
2281 #[test]
2282 fn rewrite_select_as_values_not_applicable_with_values() {
2283 assert!(code_action_not_applicable(
2284 rewrite_select_as_values,
2285 "select 1 as column1, 2 as column2 union all$0 values (3, 4);"
2286 ));
2287 }
2288
2289 #[test]
2290 fn rewrite_select_as_values_not_applicable_with_table() {
2291 assert!(code_action_not_applicable(
2292 rewrite_select_as_values,
2293 "select 1 as column1, 2 as column2 union all$0 table foo;"
2294 ));
2295 }
2296
2297 #[test]
2298 fn rewrite_select_as_values_not_applicable_intersect() {
2299 assert!(code_action_not_applicable(
2300 rewrite_select_as_values,
2301 "select 1 as column1, 2 as column2 inter$0sect select 3, 4;"
2302 ));
2303 }
2304
2305 #[test]
2306 fn rewrite_select_as_values_not_applicable_except() {
2307 assert!(code_action_not_applicable(
2308 rewrite_select_as_values,
2309 "select 1 as column1, 2 as column2 exc$0ept select 3, 4;"
2310 ));
2311 }
2312
2313 #[test]
2314 fn rewrite_from_simple() {
2315 assert_snapshot!(apply_code_action(
2316 rewrite_from,
2317 "from$0 t;"),
2318 @"select * from t;"
2319 );
2320 }
2321
2322 #[test]
2323 fn rewrite_from_qualified() {
2324 assert_snapshot!(apply_code_action(
2325 rewrite_from,
2326 "from$0 s.t;"),
2327 @"select * from s.t;"
2328 );
2329 }
2330
2331 #[test]
2332 fn rewrite_from_on_name() {
2333 assert_snapshot!(apply_code_action(
2334 rewrite_from,
2335 "from t$0;"),
2336 @"select * from t;"
2337 );
2338 }
2339
2340 #[test]
2341 fn rewrite_from_not_applicable_with_select() {
2342 assert!(code_action_not_applicable_with_errors(
2343 rewrite_from,
2344 "from$0 t select c;"
2345 ));
2346 }
2347
2348 #[test]
2349 fn rewrite_from_not_applicable_on_normal_select() {
2350 assert!(code_action_not_applicable(
2351 rewrite_from,
2352 "select * from$0 t;"
2353 ));
2354 }
2355
2356 #[test]
2357 fn rewrite_leading_from_simple() {
2358 assert_snapshot!(apply_code_action(
2359 rewrite_leading_from,
2360 "from$0 t select c;"),
2361 @"select c from t;"
2362 );
2363 }
2364
2365 #[test]
2366 fn rewrite_leading_from_multiple_cols() {
2367 assert_snapshot!(apply_code_action(
2368 rewrite_leading_from,
2369 "from$0 t select a, b;"),
2370 @"select a, b from t;"
2371 );
2372 }
2373
2374 #[test]
2375 fn rewrite_leading_from_with_where() {
2376 assert_snapshot!(apply_code_action(
2377 rewrite_leading_from,
2378 "from$0 t select c where x = 1;"),
2379 @"select c from t where x = 1;"
2380 );
2381 }
2382
2383 #[test]
2384 fn rewrite_leading_from_on_select() {
2385 assert_snapshot!(apply_code_action(
2386 rewrite_leading_from,
2387 "from t sel$0ect c;"),
2388 @"select c from t;"
2389 );
2390 }
2391
2392 #[test]
2393 fn rewrite_leading_from_not_applicable_normal() {
2394 assert!(code_action_not_applicable(
2395 rewrite_leading_from,
2396 "sel$0ect c from t;"
2397 ));
2398 }
2399
2400 #[test]
2401 fn rewrite_timestamp_without_tz_column() {
2402 assert_snapshot!(apply_code_action(
2403 rewrite_timestamp_type,
2404 "create table t(a time$0stamp without time zone);"),
2405 @"create table t(a timestamp);"
2406 );
2407 }
2408
2409 #[test]
2410 fn rewrite_timestamp_without_tz_cast() {
2411 assert_snapshot!(apply_code_action(
2412 rewrite_timestamp_type,
2413 "select timestamp$0 without time zone '2021-01-01';"),
2414 @"select timestamp '2021-01-01';"
2415 );
2416 }
2417
2418 #[test]
2419 fn rewrite_time_without_tz() {
2420 assert_snapshot!(apply_code_action(
2421 rewrite_timestamp_type,
2422 "create table t(a ti$0me without time zone);"),
2423 @"create table t(a time);"
2424 );
2425 }
2426
2427 #[test]
2428 fn rewrite_timestamp_without_tz_not_applicable_plain() {
2429 assert!(code_action_not_applicable(
2430 rewrite_timestamp_type,
2431 "create table t(a time$0stamp);"
2432 ));
2433 }
2434
2435 #[test]
2436 fn rewrite_timestamp_with_tz_column() {
2437 assert_snapshot!(apply_code_action(
2438 rewrite_timestamp_type,
2439 "create table t(a time$0stamp with time zone);"),
2440 @"create table t(a timestamptz);"
2441 );
2442 }
2443
2444 #[test]
2445 fn rewrite_timestamp_with_tz_cast() {
2446 assert_snapshot!(apply_code_action(
2447 rewrite_timestamp_type,
2448 "select timestamp$0 with time zone '2021-01-01';"),
2449 @"select timestamptz '2021-01-01';"
2450 );
2451 }
2452
2453 #[test]
2454 fn rewrite_time_with_tz() {
2455 assert_snapshot!(apply_code_action(
2456 rewrite_timestamp_type,
2457 "create table t(a ti$0me with time zone);"),
2458 @"create table t(a timetz);"
2459 );
2460 }
2461}