Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions crates/squawk_ide/src/code_actions/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,22 +12,37 @@ mod remove_else_clause;
mod remove_redundant_alias;
mod rewrite_as_dollar_quoted_string;
mod rewrite_as_regular_string;
mod rewrite_at_local_as_timezone;
mod rewrite_at_time_zone_as_timezone;
mod rewrite_between_as_binary_expression;
mod rewrite_cast_to_double_colon;
mod rewrite_collation_for_as_function_call;
mod rewrite_create_table_as_as_select_into;
mod rewrite_double_colon_to_cast;
mod rewrite_extract_as_function_call;
mod rewrite_from;
mod rewrite_in_as_expression;
mod rewrite_integer_radix;
mod rewrite_is_normalized_as_function_call;
mod rewrite_leading_from;
mod rewrite_normalize_as_function_call;
mod rewrite_not_equals_operator;
mod rewrite_overlaps_as_function_call;
mod rewrite_overlay_as_function_call;
mod rewrite_pattern_matching_as_operators;
mod rewrite_position_as_function_call;
mod rewrite_rows_from_as_unnest;
mod rewrite_select_as_table;
mod rewrite_select_as_values;
mod rewrite_select_into_as_create_table_as;
mod rewrite_substring_as_function_call;
mod rewrite_system_user_as_function_call;
mod rewrite_table_as_select;
mod rewrite_timestamp_type;
mod rewrite_trim_as_function_call;
mod rewrite_unnest_as_rows_from;
mod rewrite_values_as_select;
mod rewrite_xmlexists_as_function_call;
mod unnest;
mod unquote_identifier;

Expand All @@ -42,22 +57,37 @@ use remove_else_clause::remove_else_clause;
use remove_redundant_alias::remove_redundant_alias;
use rewrite_as_dollar_quoted_string::rewrite_as_dollar_quoted_string;
use rewrite_as_regular_string::rewrite_as_regular_string;
use rewrite_at_local_as_timezone::rewrite_at_local_as_timezone;
use rewrite_at_time_zone_as_timezone::rewrite_at_time_zone_as_timezone;
use rewrite_between_as_binary_expression::rewrite_between_as_binary_expression;
use rewrite_cast_to_double_colon::rewrite_cast_to_double_colon;
use rewrite_collation_for_as_function_call::rewrite_collation_for_as_function_call;
use rewrite_create_table_as_as_select_into::rewrite_create_table_as_as_select_into;
use rewrite_double_colon_to_cast::rewrite_double_colon_to_cast;
use rewrite_extract_as_function_call::rewrite_extract_as_function_call;
use rewrite_from::rewrite_from;
use rewrite_in_as_expression::rewrite_in_as_expression;
use rewrite_integer_radix::rewrite_integer_radix;
use rewrite_is_normalized_as_function_call::rewrite_is_normalized_as_function_call;
use rewrite_leading_from::rewrite_leading_from;
use rewrite_normalize_as_function_call::rewrite_normalize_as_function_call;
use rewrite_not_equals_operator::rewrite_not_equals_operator;
use rewrite_overlaps_as_function_call::rewrite_overlaps_as_function_call;
use rewrite_overlay_as_function_call::rewrite_overlay_as_function_call;
use rewrite_pattern_matching_as_operators::rewrite_pattern_matching_as_operators;
use rewrite_position_as_function_call::rewrite_position_as_function_call;
use rewrite_rows_from_as_unnest::rewrite_rows_from_as_unnest;
use rewrite_select_as_table::rewrite_select_as_table;
use rewrite_select_as_values::rewrite_select_as_values;
use rewrite_select_into_as_create_table_as::rewrite_select_into_as_create_table_as;
use rewrite_substring_as_function_call::rewrite_substring_as_function_call;
use rewrite_system_user_as_function_call::rewrite_system_user_as_function_call;
use rewrite_table_as_select::rewrite_table_as_select;
use rewrite_timestamp_type::rewrite_timestamp_type;
use rewrite_trim_as_function_call::rewrite_trim_as_function_call;
use rewrite_unnest_as_rows_from::rewrite_unnest_as_rows_from;
use rewrite_values_as_select::rewrite_values_as_select;
use rewrite_xmlexists_as_function_call::rewrite_xmlexists_as_function_call;
use unquote_identifier::unquote_identifier;

#[derive(Debug, Clone, PartialEq, Eq)]
Expand Down Expand Up @@ -100,5 +130,20 @@ pub fn code_actions(db: &dyn Db, position: InFile<TextSize>) -> Option<Vec<CodeA
rewrite_timestamp_type(db, position, &mut actions);
rewrite_unnest_as_rows_from(db, position, &mut actions);
rewrite_rows_from_as_unnest(db, position, &mut actions);
rewrite_at_time_zone_as_timezone(db, position, &mut actions);
rewrite_at_local_as_timezone(db, position, &mut actions);
rewrite_overlaps_as_function_call(db, position, &mut actions);
rewrite_extract_as_function_call(db, position, &mut actions);
rewrite_is_normalized_as_function_call(db, position, &mut actions);
rewrite_collation_for_as_function_call(db, position, &mut actions);
rewrite_normalize_as_function_call(db, position, &mut actions);
rewrite_overlay_as_function_call(db, position, &mut actions);
rewrite_position_as_function_call(db, position, &mut actions);
rewrite_substring_as_function_call(db, position, &mut actions);
rewrite_trim_as_function_call(db, position, &mut actions);
rewrite_system_user_as_function_call(db, position, &mut actions);
rewrite_xmlexists_as_function_call(db, position, &mut actions);
rewrite_in_as_expression(db, position, &mut actions);
rewrite_pattern_matching_as_operators(db, position, &mut actions);
Some(actions)
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use rowan::TextSize;
use salsa::Database as Db;
use squawk_linter::Edit;
use squawk_syntax::{SyntaxKind, ast::AstNode};
use squawk_syntax::{SyntaxKind, ast::AstNode, quote::quote_string_literal};

use crate::db::parse;
use crate::file::InFile;
Expand Down Expand Up @@ -40,9 +40,7 @@ fn dollar_quoted_to_string(text: &str) -> Option<String> {
return None;
}

// quotes are escaped by using two of them in Postgres
let escaped = content.replace('\'', "''");
Some(format!("'{escaped}'"))
Some(quote_string_literal(content))
}

fn split_dollar_quoted(text: &str) -> Option<(String, &str)> {
Expand Down
84 changes: 84 additions & 0 deletions crates/squawk_ide/src/code_actions/rewrite_at_local_as_timezone.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
use rowan::TextSize;
use salsa::Database as Db;
use squawk_linter::Edit;
use squawk_syntax::ast::{self, AstNode};

use crate::{file::InFile, offsets::token_from_offset};

use super::{ActionKind, CodeAction};

pub(super) fn rewrite_at_local_as_timezone(
db: &dyn Db,
position: InFile<TextSize>,
actions: &mut Vec<CodeAction>,
) -> Option<()> {
let token = token_from_offset(db, position)?;
let postfix_expr = token
.parent_ancestors()
.filter_map(ast::PostfixExpr::cast)
.find(|expr| matches!(expr.op(), Some(ast::PostfixOp::AtLocal(_))))?;

let expr = postfix_expr.expr()?;
let replacement = format!("timezone({})", expr.syntax().text());

actions.push(CodeAction {
title: "Rewrite `AT LOCAL` as `timezone`".to_owned(),
edits: vec![Edit::replace(
postfix_expr.syntax().text_range(),
replacement,
)],
kind: ActionKind::RefactorRewrite,
});

Some(())
}

#[cfg(test)]
mod test {
use insta::assert_snapshot;

use crate::code_actions::test_utils::{apply_code_action, code_action_not_applicable};

use super::rewrite_at_local_as_timezone;

#[test]
fn rewrites_at_local_as_timezone() {
assert_snapshot!(
apply_code_action(
rewrite_at_local_as_timezone,
"select TIMESTAMPTZ '2026-08-17 09:30+00' AT $0LOCAL;",
),
@"select timezone(TIMESTAMPTZ '2026-08-17 09:30+00');"
);
}

#[test]
fn rewrites_with_cursor_on_operand() {
assert_snapshot!(
apply_code_action(
rewrite_at_local_as_timezone,
"select (created_$0at + interval '1 hour') AT LOCAL from events;",
),
@"select timezone((created_at + interval '1 hour')) from events;"
);
}

#[test]
fn rewrites_with_cursor_at_end_of_expression() {
assert_snapshot!(
apply_code_action(
rewrite_at_local_as_timezone,
"select created_at AT LOCAL$0;",
),
@"select timezone(created_at);"
);
}

#[test]
fn not_applicable_to_at_time_zone() {
assert!(code_action_not_applicable(
rewrite_at_local_as_timezone,
"select created_at AT TIME $0ZONE 'UTC';"
));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
use rowan::TextSize;
use salsa::Database as Db;
use squawk_linter::Edit;
use squawk_syntax::ast::{self, AstNode};

use crate::{file::InFile, offsets::token_from_offset};

use super::{ActionKind, CodeAction};

pub(super) fn rewrite_at_time_zone_as_timezone(
db: &dyn Db,
position: InFile<TextSize>,
actions: &mut Vec<CodeAction>,
) -> Option<()> {
let token = token_from_offset(db, position)?;
let bin_expr = token
.parent_ancestors()
.filter_map(ast::BinExpr::cast)
.find(|expr| matches!(expr.op(), Some(ast::BinOp::AtTimeZone(_))))?;

let value = bin_expr.lhs()?;
let zone = bin_expr.rhs()?;
let replacement = format!(
"timezone({}, {})",
zone.syntax().text(),
value.syntax().text()
);

actions.push(CodeAction {
title: "Rewrite as timezone function `timezone()`".to_owned(),
edits: vec![Edit::replace(bin_expr.syntax().text_range(), replacement)],
kind: ActionKind::RefactorRewrite,
});

Some(())
}

#[cfg(test)]
mod test {
use insta::assert_snapshot;

use crate::code_actions::test_utils::{apply_code_action, code_action_not_applicable};

use super::rewrite_at_time_zone_as_timezone;

#[test]
fn rewrites_at_time_zone_as_timezone() {
assert_snapshot!(
apply_code_action(
rewrite_at_time_zone_as_timezone,
"select TIMESTAMP '2026-08-17 09:30' AT TIME $0ZONE 'America/New_York';",
),
@"select timezone('America/New_York', TIMESTAMP '2026-08-17 09:30');"
);
}

#[test]
fn applies_from_the_value_and_preserves_surrounding_expression() {
assert_snapshot!(
apply_code_action(
rewrite_at_time_zone_as_timezone,
"select 1 + created$0_at AT TIME ZONE zone_name;",
),
@"select 1 + timezone(zone_name, created_at);"
);
}

#[test]
fn rewrites_expression_zone() {
assert_snapshot!(
apply_code_action(
rewrite_at_time_zone_as_timezone,
"select ts AT TIME ZONE coale$0sce(zone_name, 'UTC');",
),
@"select timezone(coalesce(zone_name, 'UTC'), ts);"
);
}

#[test]
fn rewrites_nearest_nested_at_time_zone() {
assert_snapshot!(
apply_code_action(
rewrite_at_time_zone_as_timezone,
"select (ts AT TIME ZONE z$01) AT TIME ZONE z2;",
),
@"select (timezone(z1, ts)) AT TIME ZONE z2;"
);
}

#[test]
fn not_applicable_outside_at_time_zone() {
assert!(code_action_not_applicable(
rewrite_at_time_zone_as_timezone,
"select created_at +$0 interval '1 hour';"
));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
use rowan::TextSize;
use salsa::Database as Db;
use squawk_linter::Edit;
use squawk_syntax::ast::{self, AstNode};

use crate::{file::InFile, offsets::token_from_offset};

use super::{ActionKind, CodeAction};

pub(super) fn rewrite_collation_for_as_function_call(
db: &dyn Db,
position: InFile<TextSize>,
actions: &mut Vec<CodeAction>,
) -> Option<()> {
let token = token_from_offset(db, position)?;
let collation_for = token
.parent_ancestors()
.find_map(ast::CollationForFn::cast)?;
let expr = collation_for.expr()?;

actions.push(CodeAction {
title: "Rewrite as `pg_collation_for` function call".to_owned(),
edits: vec![Edit::replace(
collation_for.syntax().text_range(),
format!("pg_collation_for({})", expr.syntax().text()),
)],
kind: ActionKind::RefactorRewrite,
});

Some(())
}

#[cfg(test)]
mod test {
use insta::assert_snapshot;

use crate::code_actions::test_utils::{apply_code_action, code_action_not_applicable};

use super::rewrite_collation_for_as_function_call;

#[test]
fn rewrites_collation_for_as_function_call() {
assert_snapshot!(
apply_code_action(
rewrite_collation_for_as_function_call,
"select COLLATION $0FOR ('x'::text);",
),
@"select pg_collation_for('x'::text);"
);
}

#[test]
fn rewrites_with_cursor_in_expression() {
assert_snapshot!(
apply_code_action(
rewrite_collation_for_as_function_call,
"select COLLATION FOR (lower('x'$0));",
),
@"select pg_collation_for(lower('x'));"
);
}

#[test]
fn rewrites_innermost_collation_for() {
assert_snapshot!(
apply_code_action(
rewrite_collation_for_as_function_call,
"select COLLATION FOR (COLLATION F$0OR (x));",
),
@"select COLLATION FOR (pg_collation_for(x));"
);
}

#[test]
fn not_applicable_outside_collation_for() {
assert!(code_action_not_applicable(
rewrite_collation_for_as_function_call,
"select collati$0on from t;"
));
}
}
Loading
Loading