squawk_ide/
code_actions.rs

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