Skip to main content

squawk_ide/
code_actions.rs

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    // quotes are escaped by using two of them in Postgres
118    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    // the `foo` in `select $foo$bar$foo$`
126    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    // We can't safely transform a trailing `$` i.e., `select 'foo $'` with an
146    // empty delim, because we'll  `select $$foo $$$` which isn't valid.
147    if !content.contains("$$") && !content.ends_with('$') {
148        return Some("".to_owned());
149    }
150
151    let mut delim = "q".to_owned();
152    // don't want to just loop forever
153    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
312/// Returns true if a `select` statement can be safely rewritten as a `table` statement.
313///
314/// We can only do this when there are no clauses besides the `select` and
315/// `from` clause. Additionally, we can only have a table reference in the
316/// `from` clause.
317/// The `select`'s target list must only be a `*`.
318fn 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    // only want to support: `select *`
356    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    // only can have one from item & no join exprs
370    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    // only want table refs
389    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
452// Postgres docs call these output names.
453// Postgres' parser calls this a column label.
454// Third-party docs call these aliases, so going with that.
455fn 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    // Postgres docs recommend either using `as` or quoting the name. I think
479    // `as` looks a bit nicer.
480    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    // TODO:
510    // This lets use remove any whitespace so we don't end up with:
511    //   select x as x, b from t;
512    // becoming
513    //   select x , b from t;
514    // but we probably want a better way to express this.
515    // Maybe a "Remove preceding whitespace" style option for edits.
516    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    // TODO: we should salsa this
560    let binder = binder::bind(file);
561    // TODO: we don't need the search path at the current position, we need to
562    // lookup the definition of the item and see what the definition's search
563    // path is.
564    //
565    // It tries to rewrite:
566    // `select now()::timestamptz;` as
567    // `select now()::public.timestamptz;`
568    // instead of
569    // `select now()::pg_catalog.timestamptz;`
570    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    // `values` but we skip over the possibly preceeding CTE
754    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        // Ideally we'd have something like Python's `yield` and `yield from`
815        // but instead we have to do all of this to avoid creating some temp
816        // vecs
817        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                // Quickfixes can fix syntax errors so we don't assert
958            }
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                // Quickfixes can fix syntax errors so we don't assert
983            }
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    // There's an invariant where the edits can't overlap.
997    // For example, if we have an edit that deletes the full `else clause` and
998    // another edit that deletes the `else` keyword and they overlap, then
999    // vscode doesn't surface the code action.
1000    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        // we can't insert the schema here because:
1323        // `select public.t from t` isn't valid
1324        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        // not applicable since we don't have the table name set
1334        // we'll have another quick action to insert table names
1335        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        // not valid because we haven't specified the schema on the table name
1345        // `select public.t.c from t` isn't valid sql
1346        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        // upper case
1580        assert!(code_action_not_applicable(
1581            unquote_identifier,
1582            r#"select "X"$0 from t;"#
1583        ));
1584        // upper case
1585        assert!(code_action_not_applicable(
1586            unquote_identifier,
1587            r#"select "Foo"$0 from t;"#
1588        ));
1589        // dash
1590        assert!(code_action_not_applicable(
1591            unquote_identifier,
1592            r#"select "my-col"$0 from t;"#
1593        ));
1594        // leading digits
1595        assert!(code_action_not_applicable(
1596            unquote_identifier,
1597            r#"select "123"$0 from t;"#
1598        ));
1599        // space
1600        assert!(code_action_not_applicable(
1601            unquote_identifier,
1602            r#"select "foo bar"$0 from t;"#
1603        ));
1604        // quotes
1605        assert!(code_action_not_applicable(
1606            unquote_identifier,
1607            r#"select "foo""bar"$0 from t;"#
1608        ));
1609        // already unquoted
1610        assert!(code_action_not_applicable(
1611            unquote_identifier,
1612            "select x$0 from t;"
1613        ));
1614        // brackets
1615        assert!(code_action_not_applicable(
1616            unquote_identifier,
1617            r#"select "my[col]"$0 from t;"#
1618        ));
1619        // curly brackets
1620        assert!(code_action_not_applicable(
1621            unquote_identifier,
1622            r#"select "my{}"$0 from t;"#
1623        ));
1624        // reserved word
1625        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}