squawk_ide/
code_actions.rs

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