11use rowan:: TextSize ;
22use squawk_linter:: Edit ;
33use squawk_syntax:: {
4- SyntaxKind , SyntaxNode ,
4+ SyntaxKind ,
55 ast:: { self , AstNode } ,
66} ;
77
8- use crate :: { generated:: keywords:: RESERVED_KEYWORDS , offsets:: token_from_offset} ;
8+ use crate :: {
9+ column_name:: ColumnName ,
10+ offsets:: token_from_offset,
11+ quote:: { quote_column_alias, unquote_ident} ,
12+ } ;
913
1014#[ derive( Debug , Clone ) ]
1115pub enum ActionKind {
@@ -29,6 +33,7 @@ pub fn code_actions(file: ast::SourceFile, offset: TextSize) -> Option<Vec<CodeA
2933 rewrite_select_as_table ( & mut actions, & file, offset) ;
3034 quote_identifier ( & mut actions, & file, offset) ;
3135 unquote_identifier ( & mut actions, & file, offset) ;
36+ add_explicit_alias ( & mut actions, & file, offset) ;
3237 Some ( actions)
3338}
3439
@@ -346,7 +351,7 @@ fn unquote_identifier(
346351 return None ;
347352 } ;
348353
349- let unquoted = unquote ( & name_node) ?;
354+ let unquoted = unquote_ident ( & name_node) ?;
350355
351356 actions. push ( CodeAction {
352357 title : "Unquote identifier" . to_owned ( ) ,
@@ -357,45 +362,37 @@ fn unquote_identifier(
357362 Some ( ( ) )
358363}
359364
360- fn unquote ( node : & SyntaxNode ) -> Option < String > {
361- let text = node. text ( ) . to_string ( ) ;
362-
363- if !text. starts_with ( '"' ) || !text. ends_with ( '"' ) {
364- return None ;
365- }
366-
367- let text = & text[ 1 ..text. len ( ) - 1 ] ;
368-
369- if is_reserved_word ( text) {
370- return None ;
371- }
365+ // Postgres docs call these output names.
366+ // Postgres' parser calls this a column label.
367+ // Third-party docs call these aliases, so going with that.
368+ fn add_explicit_alias (
369+ actions : & mut Vec < CodeAction > ,
370+ file : & ast:: SourceFile ,
371+ offset : TextSize ,
372+ ) -> Option < ( ) > {
373+ let token = token_from_offset ( file, offset) ?;
374+ let target = token. parent_ancestors ( ) . find_map ( ast:: Target :: cast) ?;
372375
373- if text . is_empty ( ) {
376+ if target . as_name ( ) . is_some ( ) {
374377 return None ;
375378 }
376379
377- let mut chars = text . chars ( ) ;
380+ let alias = ColumnName :: from_target ( target . clone ( ) ) . and_then ( |c| c . 0 . to_string ( ) ) ? ;
378381
379- // see: https://www.postgresql.org/docs/18/sql-syntax-lexical.html#SQL-SYNTAX-IDENTIFIERS
380- match chars. next ( ) {
381- Some ( c) if c. is_lowercase ( ) || c == '_' => { }
382- _ => return None ,
383- }
382+ let expr_end = target. expr ( ) . map ( |e| e. syntax ( ) . text_range ( ) . end ( ) ) ?;
384383
385- for c in chars {
386- if c. is_lowercase ( ) || c. is_ascii_digit ( ) || c == '_' || c == '$' {
387- continue ;
388- }
389- return None ;
390- }
384+ let quoted_alias = quote_column_alias ( & alias) ;
385+ // Postgres docs recommend either using `as` or quoting the name. I think
386+ // `as` looks a bit nicer.
387+ let replacement = format ! ( " as {}" , quoted_alias) ;
391388
392- Some ( text. to_string ( ) )
393- }
389+ actions. push ( CodeAction {
390+ title : "Add explicit alias" . to_owned ( ) ,
391+ edits : vec ! [ Edit :: insert( replacement, expr_end) ] ,
392+ kind : ActionKind :: RefactorRewrite ,
393+ } ) ;
394394
395- fn is_reserved_word ( text : & str ) -> bool {
396- RESERVED_KEYWORDS
397- . binary_search ( & text. to_lowercase ( ) . as_str ( ) )
398- . is_ok ( )
395+ Some ( ( ) )
399396}
400397
401398#[ cfg( test) ]
@@ -933,4 +930,119 @@ mod test {
933930 @"create table T(x int);"
934931 ) ;
935932 }
933+
934+ #[ test]
935+ fn add_explicit_alias_simple_column ( ) {
936+ assert_snapshot ! ( apply_code_action(
937+ add_explicit_alias,
938+ "select col_na$0me from t;" ) ,
939+ @"select col_name as col_name from t;"
940+ ) ;
941+ }
942+
943+ #[ test]
944+ fn add_explicit_alias_quoted_identifier ( ) {
945+ assert_snapshot ! ( apply_code_action(
946+ add_explicit_alias,
947+ r#"select "b"$0 from t;"# ) ,
948+ @r#"select "b" as b from t;"#
949+ ) ;
950+ }
951+
952+ #[ test]
953+ fn add_explicit_alias_field_expr ( ) {
954+ assert_snapshot ! ( apply_code_action(
955+ add_explicit_alias,
956+ "select t.col$0umn from t;" ) ,
957+ @"select t.column as column from t;"
958+ ) ;
959+ }
960+
961+ #[ test]
962+ fn add_explicit_alias_function_call ( ) {
963+ assert_snapshot ! ( apply_code_action(
964+ add_explicit_alias,
965+ "select cou$0nt(*) from t;" ) ,
966+ @"select count(*) as count from t;"
967+ ) ;
968+ }
969+
970+ #[ test]
971+ fn add_explicit_alias_cast_to_type ( ) {
972+ assert_snapshot ! ( apply_code_action(
973+ add_explicit_alias,
974+ "select '1'::bigi$0nt from t;" ) ,
975+ @"select '1'::bigint as int8 from t;"
976+ ) ;
977+ }
978+
979+ #[ test]
980+ fn add_explicit_alias_cast_column ( ) {
981+ assert_snapshot ! ( apply_code_action(
982+ add_explicit_alias,
983+ "select col_na$0me::text from t;" ) ,
984+ @"select col_name::text as col_name from t;"
985+ ) ;
986+ }
987+
988+ #[ test]
989+ fn add_explicit_alias_case_expr ( ) {
990+ assert_snapshot ! ( apply_code_action(
991+ add_explicit_alias,
992+ "select ca$0se when true then 'a' end from t;" ) ,
993+ @"select case when true then 'a' end as case from t;"
994+ ) ;
995+ }
996+
997+ #[ test]
998+ fn add_explicit_alias_case_with_else ( ) {
999+ assert_snapshot ! ( apply_code_action(
1000+ add_explicit_alias,
1001+ "select ca$0se when true then 'a' else now()::text end from t;" ) ,
1002+ @"select case when true then 'a' else now()::text end as now from t;"
1003+ ) ;
1004+ }
1005+
1006+ #[ test]
1007+ fn add_explicit_alias_array ( ) {
1008+ assert_snapshot ! ( apply_code_action(
1009+ add_explicit_alias,
1010+ "select arr$0ay[1, 2, 3] from t;" ) ,
1011+ @"select array[1, 2, 3] as array from t;"
1012+ ) ;
1013+ }
1014+
1015+ #[ test]
1016+ fn add_explicit_alias_not_applicable_already_has_alias ( ) {
1017+ assert ! ( code_action_not_applicable(
1018+ add_explicit_alias,
1019+ "select col_name$0 as foo from t;"
1020+ ) ) ;
1021+ }
1022+
1023+ #[ test]
1024+ fn add_explicit_alias_unknown_column ( ) {
1025+ assert_snapshot ! ( apply_code_action(
1026+ add_explicit_alias,
1027+ "select 1 $0+ 2 from t;" ) ,
1028+ @r#"select 1 + 2 as "?column?" from t;"#
1029+ ) ;
1030+ }
1031+
1032+ #[ test]
1033+ fn add_explicit_alias_not_applicable_star ( ) {
1034+ assert ! ( code_action_not_applicable(
1035+ add_explicit_alias,
1036+ "select $0* from t;"
1037+ ) ) ;
1038+ }
1039+
1040+ #[ test]
1041+ fn add_explicit_alias_literal ( ) {
1042+ assert_snapshot ! ( apply_code_action(
1043+ add_explicit_alias,
1044+ "select 'foo$0' from t;" ) ,
1045+ @r#"select 'foo' as "?column?" from t;"#
1046+ ) ;
1047+ }
9361048}
0 commit comments