diff --git a/crates/squawk_ide/src/classify.rs b/crates/squawk_ide/src/classify.rs index 3fefdc3a..6e179078 100644 --- a/crates/squawk_ide/src/classify.rs +++ b/crates/squawk_ide/src/classify.rs @@ -56,6 +56,17 @@ pub(crate) enum NameRefClass { JoinUsingColumn, SchemaQualifier, TypeReference, + TruncateTable, + LockTable, + VacuumTable, + AlterTable, + AlterTableColumn, + RefreshMaterializedView, + ReindexTable, + ReindexIndex, + ReindexSchema, + ReindexDatabase, + ReindexSystem, } pub(crate) fn classify_name_ref(name_ref: &ast::NameRef) -> Option { @@ -192,6 +203,41 @@ pub(crate) fn classify_name_ref(name_ref: &ast::NameRef) -> Option if ast::DropTable::can_cast(ancestor.kind()) { return Some(NameRefClass::DropTable); } + if ast::Truncate::can_cast(ancestor.kind()) { + return Some(NameRefClass::TruncateTable); + } + if ast::Lock::can_cast(ancestor.kind()) { + return Some(NameRefClass::LockTable); + } + if ast::Vacuum::can_cast(ancestor.kind()) { + return Some(NameRefClass::VacuumTable); + } + if ast::AlterColumn::can_cast(ancestor.kind()) { + return Some(NameRefClass::AlterTableColumn); + } + if ast::AlterTable::can_cast(ancestor.kind()) { + return Some(NameRefClass::AlterTable); + } + if ast::Refresh::can_cast(ancestor.kind()) { + return Some(NameRefClass::RefreshMaterializedView); + } + if let Some(reindex) = ast::Reindex::cast(ancestor.clone()) { + if reindex.table_token().is_some() { + return Some(NameRefClass::ReindexTable); + } + if reindex.index_token().is_some() { + return Some(NameRefClass::ReindexIndex); + } + if reindex.schema_token().is_some() { + return Some(NameRefClass::ReindexSchema); + } + if reindex.database_token().is_some() { + return Some(NameRefClass::ReindexDatabase); + } + if reindex.system_token().is_some() { + return Some(NameRefClass::ReindexSystem); + } + } if ast::Table::can_cast(ancestor.kind()) { return Some(NameRefClass::Table); } diff --git a/crates/squawk_ide/src/goto_definition.rs b/crates/squawk_ide/src/goto_definition.rs index 13695b77..59085a3b 100644 --- a/crates/squawk_ide/src/goto_definition.rs +++ b/crates/squawk_ide/src/goto_definition.rs @@ -4780,4 +4780,260 @@ update users set email = new_data.column2$0 from new_data where users.id = new_d ╰╴ ─ 1. source "); } + + #[test] + fn goto_truncate_table() { + assert_snapshot!(goto(" +create table t(); +truncate table t$0; +"), @r" + ╭▸ + 2 │ create table t(); + │ ─ 2. destination + 3 │ truncate table t; + ╰╴ ─ 1. source + "); + } + + #[test] + fn goto_truncate_table_without_table_keyword() { + assert_snapshot!(goto(" +create table t(); +truncate t$0; +"), @r" + ╭▸ + 2 │ create table t(); + │ ─ 2. destination + 3 │ truncate t; + ╰╴ ─ 1. source + "); + } + + #[test] + fn goto_truncate_multiple_tables() { + assert_snapshot!(goto(" +create table t1(); +create table t2(); +truncate t1, t2$0; +"), @r" + ╭▸ + 3 │ create table t2(); + │ ── 2. destination + 4 │ truncate t1, t2; + ╰╴ ─ 1. source + "); + } + + #[test] + fn goto_lock_table() { + assert_snapshot!(goto(" +create table t(); +lock table t$0; +"), @r" + ╭▸ + 2 │ create table t(); + │ ─ 2. destination + 3 │ lock table t; + ╰╴ ─ 1. source + "); + } + + #[test] + fn goto_lock_table_without_table_keyword() { + assert_snapshot!(goto(" +create table t(); +lock t$0; +"), @r" + ╭▸ + 2 │ create table t(); + │ ─ 2. destination + 3 │ lock t; + ╰╴ ─ 1. source + "); + } + + #[test] + fn goto_lock_multiple_tables() { + assert_snapshot!(goto(" +create table t1(); +create table t2(); +lock t1, t2$0; +"), @r" + ╭▸ + 3 │ create table t2(); + │ ── 2. destination + 4 │ lock t1, t2; + ╰╴ ─ 1. source + "); + } + + #[test] + fn goto_vacuum_table() { + assert_snapshot!(goto(" +create table users(id int, email text); +vacuum users$0; +"), @r" + ╭▸ + 2 │ create table users(id int, email text); + │ ───── 2. destination + 3 │ vacuum users; + ╰╴ ─ 1. source + "); + } + + #[test] + fn goto_vacuum_multiple_tables() { + assert_snapshot!(goto(" +create table t1(); +create table t2(); +vacuum t1, t2$0; +"), @r" + ╭▸ + 3 │ create table t2(); + │ ── 2. destination + 4 │ vacuum t1, t2; + ╰╴ ─ 1. source + "); + } + + #[test] + fn goto_alter_table() { + assert_snapshot!(goto(" +create table users(id int, email text); +alter table users$0 alter email set not null; +"), @r" + ╭▸ + 2 │ create table users(id int, email text); + │ ───── 2. destination + 3 │ alter table users alter email set not null; + ╰╴ ─ 1. source + "); + } + + #[test] + fn goto_alter_table_column() { + assert_snapshot!(goto(" +create table users(id int, email text); +alter table users alter email$0 set not null; +"), @r" + ╭▸ + 2 │ create table users(id int, email text); + │ ───── 2. destination + 3 │ alter table users alter email set not null; + ╰╴ ─ 1. source + "); + } + + #[test] + fn goto_alter_table_column_with_column_keyword() { + assert_snapshot!(goto(" +create table users(id int, email text); +alter table users alter column email$0 set not null; +"), @r" + ╭▸ + 2 │ create table users(id int, email text); + │ ───── 2. destination + 3 │ alter table users alter column email set not null; + ╰╴ ─ 1. source + "); + } + + #[test] + fn goto_refresh_materialized_view() { + assert_snapshot!(goto(" +create materialized view mv as select 1; +refresh materialized view mv$0; +"), @r" + ╭▸ + 2 │ create materialized view mv as select 1; + │ ── 2. destination + 3 │ refresh materialized view mv; + ╰╴ ─ 1. source + "); + } + + #[test] + fn goto_refresh_materialized_view_concurrently() { + assert_snapshot!(goto(" +create materialized view mv as select 1; +refresh materialized view concurrently mv$0; +"), @r" + ╭▸ + 2 │ create materialized view mv as select 1; + │ ── 2. destination + 3 │ refresh materialized view concurrently mv; + ╰╴ ─ 1. source + "); + } + + #[test] + fn goto_reindex_table() { + assert_snapshot!(goto(" +create table users(id int); +reindex table users$0; +"), @r" + ╭▸ + 2 │ create table users(id int); + │ ───── 2. destination + 3 │ reindex table users; + ╰╴ ─ 1. source + "); + } + + #[test] + fn goto_reindex_index() { + assert_snapshot!(goto(" +create table t(c int); +create index idx on t(c); +reindex index idx$0; +"), @r" + ╭▸ + 3 │ create index idx on t(c); + │ ─── 2. destination + 4 │ reindex index idx; + ╰╴ ─ 1. source + "); + } + + #[test] + fn goto_reindex_schema() { + assert_snapshot!(goto(" +create schema app; +reindex schema app$0; +"), @r" + ╭▸ + 2 │ create schema app; + │ ─── 2. destination + 3 │ reindex schema app; + ╰╴ ─ 1. source + "); + } + + #[test] + fn goto_reindex_database() { + assert_snapshot!(goto(" +create database appdb; +reindex database appdb$0; +"), @r" + ╭▸ + 2 │ create database appdb; + │ ───── 2. destination + 3 │ reindex database appdb; + ╰╴ ─ 1. source + "); + } + + #[test] + fn goto_reindex_system() { + assert_snapshot!(goto(" +create database systemdb; +reindex system systemdb$0; +"), @r" + ╭▸ + 2 │ create database systemdb; + │ ──────── 2. destination + 3 │ reindex system systemdb; + ╰╴ ─ 1. source + "); + } } diff --git a/crates/squawk_ide/src/hover.rs b/crates/squawk_ide/src/hover.rs index 8ead853f..e53e3dfe 100644 --- a/crates/squawk_ide/src/hover.rs +++ b/crates/squawk_ide/src/hover.rs @@ -54,7 +54,11 @@ pub fn hover(file: &ast::SourceFile, offset: TextSize) -> Option { | NameRefClass::NotNullConstraintColumn | NameRefClass::ExcludeConstraintColumn | NameRefClass::PartitionByColumn - | NameRefClass::JoinUsingColumn => { + | NameRefClass::JoinUsingColumn + | NameRefClass::ForeignKeyColumn + | NameRefClass::ForeignKeyLocalColumn + | NameRefClass::SequenceOwnedByColumn + | NameRefClass::AlterTableColumn => { return hover_column(root, &name_ref, &binder); } NameRefClass::TypeReference | NameRefClass::DropType => { @@ -89,16 +93,19 @@ pub fn hover(file: &ast::SourceFile, offset: TextSize) -> Option { | NameRefClass::ForeignKeyTable | NameRefClass::LikeTable | NameRefClass::InheritsTable - | NameRefClass::PartitionOfTable => { + | NameRefClass::PartitionOfTable + | NameRefClass::TruncateTable + | NameRefClass::LockTable + | NameRefClass::VacuumTable + | NameRefClass::AlterTable + | NameRefClass::ReindexTable + | NameRefClass::RefreshMaterializedView => { return hover_table(root, &name_ref, &binder); } - NameRefClass::ForeignKeyColumn - | NameRefClass::ForeignKeyLocalColumn - | NameRefClass::SequenceOwnedByColumn => { - return hover_column(root, &name_ref, &binder); - } NameRefClass::DropSequence => return hover_sequence(root, &name_ref, &binder), - NameRefClass::DropDatabase => return hover_database(root, &name_ref, &binder), + NameRefClass::DropDatabase + | NameRefClass::ReindexDatabase + | NameRefClass::ReindexSystem => return hover_database(root, &name_ref, &binder), NameRefClass::DropServer | NameRefClass::AlterServer | NameRefClass::CreateServer @@ -106,16 +113,17 @@ pub fn hover(file: &ast::SourceFile, offset: TextSize) -> Option { return hover_server(root, &name_ref, &binder); } NameRefClass::Tablespace => return hover_tablespace(root, &name_ref, &binder), - NameRefClass::DropIndex => return hover_index(root, &name_ref, &binder), - NameRefClass::DropFunction => return hover_function(root, &name_ref, &binder), + NameRefClass::DropIndex | NameRefClass::ReindexIndex => { + return hover_index(root, &name_ref, &binder); + } + NameRefClass::DropFunction | NameRefClass::DefaultConstraintFunctionCall => { + return hover_function(root, &name_ref, &binder); + } NameRefClass::DropAggregate => return hover_aggregate(root, &name_ref, &binder), NameRefClass::DropProcedure | NameRefClass::CallProcedure => { return hover_procedure(root, &name_ref, &binder); } NameRefClass::DropRoutine => return hover_routine(root, &name_ref, &binder), - NameRefClass::DefaultConstraintFunctionCall => { - return hover_function(root, &name_ref, &binder); - } NameRefClass::SelectFunctionCall => { // Try function first, but fall back to column if no function found // (handles function-call-style column access like `select a(t)`) @@ -126,7 +134,8 @@ pub fn hover(file: &ast::SourceFile, offset: TextSize) -> Option { } NameRefClass::SchemaQualifier | NameRefClass::DropSchema - | NameRefClass::CreateSchema => { + | NameRefClass::CreateSchema + | NameRefClass::ReindexSchema => { return hover_schema(root, &name_ref, &binder); } } @@ -367,6 +376,9 @@ fn hover_table_from_ptr( match resolve::find_table_source(&table_name_node)? { resolve::TableSource::WithTable(with_table) => format_with_table(&with_table), resolve::TableSource::CreateView(create_view) => format_create_view(&create_view, binder), + resolve::TableSource::CreateMaterializedView(create_materialized_view) => { + format_create_materialized_view(&create_materialized_view, binder) + } resolve::TableSource::CreateTable(create_table) => { format_create_table(&create_table, binder) } @@ -461,6 +473,9 @@ fn hover_qualified_star_columns( resolve::TableSource::CreateView(create_view) => { hover_qualified_star_columns_from_view(&create_view, binder) } + resolve::TableSource::CreateMaterializedView(create_materialized_view) => { + hover_qualified_star_columns_from_materialized_view(&create_materialized_view, binder) + } } } @@ -533,6 +548,29 @@ fn hover_qualified_star_columns_from_view( Some(results.join("\n")) } +fn hover_qualified_star_columns_from_materialized_view( + create_materialized_view: &ast::CreateMaterializedView, + binder: &binder::Binder, +) -> Option { + let path = create_materialized_view.path()?; + let (schema, view_name) = resolve::resolve_view_info(binder, &path)?; + + let schema_str = schema.to_string(); + let column_names = resolve::collect_materialized_view_column_names(create_materialized_view); + let results: Vec = column_names + .iter() + .map(|column_name| { + ColumnHover::schema_table_column(&schema_str, &view_name, &column_name.to_string()) + }) + .collect(); + + if results.is_empty() { + return None; + } + + Some(results.join("\n")) +} + fn hover_qualified_star_columns_from_subquery( root: &SyntaxNode, paren_select: &ast::ParenSelect, @@ -729,6 +767,31 @@ fn format_create_view(create_view: &ast::CreateView, binder: &binder::Binder) -> )) } +fn format_create_materialized_view( + create_materialized_view: &ast::CreateMaterializedView, + binder: &binder::Binder, +) -> Option { + let path = create_materialized_view.path()?; + let (schema, view_name) = resolve::resolve_view_info(binder, &path)?; + let schema = schema.to_string(); + + let column_list = create_materialized_view + .column_list() + .map(|cl| cl.syntax().text().to_string()) + .unwrap_or_default(); + + let query = create_materialized_view + .query()? + .syntax() + .text() + .to_string(); + + Some(format!( + "materialized view {}.{}{} as {}", + schema, view_name, column_list, query + )) +} + fn format_view_column( create_view: &ast::CreateView, column_name: Name, @@ -3171,4 +3234,148 @@ select * from t join u using (id$0); ╰╴ ─ hover "); } + + #[test] + fn hover_on_truncate_table() { + assert_snapshot!(check_hover(" +create table users(id int, email text); +truncate table users$0; +"), @r" + hover: table public.users(id int, email text) + ╭▸ + 3 │ truncate table users; + ╰╴ ─ hover + "); + } + + #[test] + fn hover_on_truncate_table_without_table_keyword() { + assert_snapshot!(check_hover(" +create table users(id int, email text); +truncate users$0; +"), @r" + hover: table public.users(id int, email text) + ╭▸ + 3 │ truncate users; + ╰╴ ─ hover + "); + } + + #[test] + fn hover_on_lock_table() { + assert_snapshot!(check_hover(" +create table users(id int, email text); +lock table users$0; +"), @r" + hover: table public.users(id int, email text) + ╭▸ + 3 │ lock table users; + ╰╴ ─ hover + "); + } + + #[test] + fn hover_on_lock_table_without_table_keyword() { + assert_snapshot!(check_hover(" +create table users(id int, email text); +lock users$0; +"), @r" + hover: table public.users(id int, email text) + ╭▸ + 3 │ lock users; + ╰╴ ─ hover + "); + } + + #[test] + fn hover_on_vacuum_table() { + assert_snapshot!(check_hover(" +create table users(id int, email text); +vacuum users$0; +"), @r" + hover: table public.users(id int, email text) + ╭▸ + 3 │ vacuum users; + ╰╴ ─ hover + "); + } + + #[test] + fn hover_on_vacuum_with_analyze() { + assert_snapshot!(check_hover(" +create table users(id int, email text); +vacuum analyze users$0; +"), @r" + hover: table public.users(id int, email text) + ╭▸ + 3 │ vacuum analyze users; + ╰╴ ─ hover + "); + } + + #[test] + fn hover_on_alter_table() { + assert_snapshot!(check_hover(" +create table users(id int, email text); +alter table users$0 alter email set not null; +"), @r" + hover: table public.users(id int, email text) + ╭▸ + 3 │ alter table users alter email set not null; + ╰╴ ─ hover + "); + } + + #[test] + fn hover_on_alter_table_column() { + assert_snapshot!(check_hover(" +create table users(id int, email text); +alter table users alter email$0 set not null; +"), @r" + hover: column public.users.email text + ╭▸ + 3 │ alter table users alter email set not null; + ╰╴ ─ hover + "); + } + + #[test] + fn hover_on_refresh_materialized_view() { + assert_snapshot!(check_hover(" +create materialized view mv as select 1; +refresh materialized view mv$0; +"), @r" + hover: materialized view public.mv as select 1 + ╭▸ + 3 │ refresh materialized view mv; + ╰╴ ─ hover + "); + } + + #[test] + fn hover_on_reindex_table() { + assert_snapshot!(check_hover(" +create table users(id int); +reindex table users$0; +"), @r" + hover: table public.users(id int) + ╭▸ + 3 │ reindex table users; + ╰╴ ─ hover + "); + } + + #[test] + fn hover_on_reindex_index() { + assert_snapshot!(check_hover(" +create table t(c int); +create index idx on t(c); +reindex index idx$0; +"), @r" + hover: index public.idx on public.t(c) + ╭▸ + 4 │ reindex index idx; + ╰╴ ─ hover + "); + } } diff --git a/crates/squawk_ide/src/resolve.rs b/crates/squawk_ide/src/resolve.rs index 5876963b..f909ddcd 100644 --- a/crates/squawk_ide/src/resolve.rs +++ b/crates/squawk_ide/src/resolve.rs @@ -26,7 +26,12 @@ pub(crate) fn resolve_name_ref( | NameRefClass::DeleteTable | NameRefClass::UpdateTable | NameRefClass::PartitionOfTable - | NameRefClass::InheritsTable => { + | NameRefClass::InheritsTable + | NameRefClass::TruncateTable + | NameRefClass::LockTable + | NameRefClass::VacuumTable + | NameRefClass::AlterTable + | NameRefClass::ReindexTable => { let path = find_containing_path(name_ref)?; let table_name = extract_table_name(&path)?; let schema = extract_schema_name(&path); @@ -59,7 +64,7 @@ pub(crate) fn resolve_name_ref( resolve_view(binder, &table_name, &schema, position).map(|ptr| smallvec![ptr]) } - NameRefClass::DropIndex => { + NameRefClass::DropIndex | NameRefClass::ReindexIndex => { let path = find_containing_path(name_ref)?; let index_name = extract_table_name(&path)?; let schema = extract_schema_name(&path); @@ -91,7 +96,9 @@ pub(crate) fn resolve_name_ref( let position = name_ref.syntax().text_range().start(); resolve_type(binder, &type_name, &schema, position).map(|ptr| smallvec![ptr]) } - NameRefClass::DropView | NameRefClass::DropMaterializedView => { + NameRefClass::DropView + | NameRefClass::DropMaterializedView + | NameRefClass::RefreshMaterializedView => { let path = find_containing_path(name_ref)?; let view_name = extract_table_name(&path)?; let schema = extract_schema_name(&path); @@ -105,7 +112,9 @@ pub(crate) fn resolve_name_ref( let position = name_ref.syntax().text_range().start(); resolve_sequence(binder, &sequence_name, &schema, position).map(|ptr| smallvec![ptr]) } - NameRefClass::DropDatabase => { + NameRefClass::ReindexDatabase + | NameRefClass::ReindexSystem + | NameRefClass::DropDatabase => { let database_name = Name::from_node(name_ref); resolve_database(binder, &database_name).map(|ptr| smallvec![ptr]) } @@ -376,6 +385,21 @@ pub(crate) fn resolve_name_ref( resolve_view(binder, &table_name, &schema, position).map(|ptr| smallvec![ptr]) } + NameRefClass::AlterTableColumn => { + let column_name = Name::from_node(name_ref); + let alter_table = name_ref + .syntax() + .ancestors() + .find_map(ast::AlterTable::cast)?; + let relation_name = alter_table.relation_name()?; + let path = relation_name.path()?; + resolve_column_for_path(binder, root, &path, column_name).map(|ptr| smallvec![ptr]) + } + NameRefClass::ReindexSchema => { + let path = find_containing_path(name_ref)?; + let schema_name = extract_table_name(&path)?; + resolve_schema(binder, &schema_name).map(|ptr| smallvec![ptr]) + } } } @@ -1706,6 +1730,7 @@ fn collect_tables_from_item( pub(crate) enum TableSource { WithTable(ast::WithTable), CreateView(ast::CreateView), + CreateMaterializedView(ast::CreateMaterializedView), CreateTable(ast::CreateTableLike), } @@ -1719,6 +1744,13 @@ pub(crate) fn find_table_source(node: &SyntaxNode) -> Option { return Some(TableSource::CreateView(create_view)); } + if let Some(create_materialized_view) = ast::CreateMaterializedView::cast(ancestor.clone()) + { + return Some(TableSource::CreateMaterializedView( + create_materialized_view, + )); + } + if let Some(create_table) = ast::CreateTableLike::cast(ancestor.clone()) { return Some(TableSource::CreateTable(create_table)); } @@ -2101,6 +2133,39 @@ pub(crate) fn collect_view_column_names(create_view: &ast::CreateView) -> Vec Vec { + if let Some(column_list) = create_materialized_view.column_list() { + let columns = collect_column_names_from_column_list(&column_list); + if !columns.is_empty() { + return columns; + } + } + + let Some(select) = select_from_materialized_view_query(create_materialized_view) else { + return vec![]; + }; + let Some(select_clause) = select.select_clause() else { + return vec![]; + }; + let Some(target_list) = select_clause.target_list() else { + return vec![]; + }; + + collect_target_list_column_names(&target_list) +} + +fn select_from_materialized_view_query( + create_materialized_view: &ast::CreateMaterializedView, +) -> Option { + let query = create_materialized_view.query()?; + match query { + ast::SelectVariant::Select(select) => Some(select), + _ => None, + } +} + pub(crate) fn collect_with_table_column_names(with_table: &ast::WithTable) -> Vec { if let Some(column_list) = with_table.column_list() { let columns = collect_column_names_from_column_list(&column_list);