From 23ec8e8d3c9d2ca8ee636d44233eb884980e9109 Mon Sep 17 00:00:00 2001 From: Steve Dignam Date: Sat, 27 Dec 2025 21:39:52 -0500 Subject: [PATCH 1/2] ide: support hover for delete, insert, and select targets --- crates/squawk_ide/src/goto_definition.rs | 57 +++++ crates/squawk_ide/src/hover.rs | 259 +++++++++++++++++++++-- crates/squawk_ide/src/resolve.rs | 51 +++++ 3 files changed, 354 insertions(+), 13 deletions(-) diff --git a/crates/squawk_ide/src/goto_definition.rs b/crates/squawk_ide/src/goto_definition.rs index a185725e..ca47b4d8 100644 --- a/crates/squawk_ide/src/goto_definition.rs +++ b/crates/squawk_ide/src/goto_definition.rs @@ -1340,4 +1340,61 @@ create table users(id int, email text); ╰╴ ───── 2. destination "); } + + #[test] + fn goto_select_column() { + assert_snapshot!(goto(" +create table users(id int, email text); +select id$0 from users; +"), @r" + ╭▸ + 2 │ create table users(id int, email text); + │ ── 2. destination + 3 │ select id from users; + ╰╴ ─ 1. source + "); + } + + #[test] + fn goto_select_column_second() { + assert_snapshot!(goto(" +create table users(id int, email text); +select id, email$0 from users; +"), @r" + ╭▸ + 2 │ create table users(id int, email text); + │ ───── 2. destination + 3 │ select id, email from users; + ╰╴ ─ 1. source + "); + } + + #[test] + fn goto_select_column_with_schema() { + assert_snapshot!(goto(" +create table public.users(id int, email text); +select email$0 from public.users; +"), @r" + ╭▸ + 2 │ create table public.users(id int, email text); + │ ───── 2. destination + 3 │ select email from public.users; + ╰╴ ─ 1. source + "); + } + + #[test] + fn goto_select_column_with_search_path() { + assert_snapshot!(goto(" +set search_path to foo; +create table foo.users(id int, email text); +select id$0 from users; +"), @r" + ╭▸ + 3 │ create table foo.users(id int, email text); + │ ── 2. destination + 4 │ select id from users; + ╰╴ ─ 1. source + "); + } } diff --git a/crates/squawk_ide/src/hover.rs b/crates/squawk_ide/src/hover.rs index d01765fd..3b276445 100644 --- a/crates/squawk_ide/src/hover.rs +++ b/crates/squawk_ide/src/hover.rs @@ -15,6 +15,10 @@ pub fn hover(file: &ast::SourceFile, offset: TextSize) -> Option { return hover_column(file, &name_ref, &binder); } + if is_select_column(&name_ref) { + return hover_column(file, &name_ref, &binder); + } + if is_table_ref(&name_ref) { return hover_table(file, &name_ref, &binder); } @@ -68,27 +72,28 @@ fn hover_column( name_ref: &ast::NameRef, binder: &binder::Binder, ) -> Option { - let column_name = name_ref.syntax().text().to_string(); - - let create_index = name_ref - .syntax() - .ancestors() - .find_map(ast::CreateIndex::cast)?; - - let relation_name = create_index.relation_name()?; - let path = relation_name.path()?; - - let (schema, table_name) = resolve::resolve_table_info(binder, &path)?; - let column_ptr = resolve::resolve_name_ref(binder, name_ref)?; let root = file.syntax(); let column_name_node = column_ptr.to_node(root); let column = column_name_node.ancestors().find_map(ast::Column::cast)?; - + let column_name = column.name()?.syntax().text().to_string(); let ty = column.ty()?; + let create_table = column + .syntax() + .ancestors() + .find_map(ast::CreateTable::cast)?; + let path = create_table.path()?; + let table_name = path.segment()?.name()?.syntax().text().to_string(); + + let schema = if let Some(qualifier) = path.qualifier() { + qualifier.syntax().text().to_string() + } else { + table_schema(&create_table, binder)? + }; + Some(format!( "{schema}.{table_name}.{column_name} {}", ty.syntax().text() @@ -216,6 +221,8 @@ fn index_schema(create_index: &ast::CreateIndex, binder: &binder::Binder) -> Opt fn is_column_ref(name_ref: &ast::NameRef) -> bool { let mut in_partition_item = false; + let mut in_column_list = false; + let mut in_where_clause = false; for ancestor in name_ref.syntax().ancestors() { if ast::PartitionItem::can_cast(ancestor.kind()) { @@ -224,12 +231,26 @@ fn is_column_ref(name_ref: &ast::NameRef) -> bool { if ast::CreateIndex::can_cast(ancestor.kind()) { return in_partition_item; } + if ast::ColumnList::can_cast(ancestor.kind()) { + in_column_list = true; + } + if ast::Insert::can_cast(ancestor.kind()) { + return in_column_list; + } + if ast::WhereClause::can_cast(ancestor.kind()) { + in_where_clause = true; + } + if ast::Delete::can_cast(ancestor.kind()) { + return in_where_clause; + } } false } fn is_table_ref(name_ref: &ast::NameRef) -> bool { let mut in_partition_item = false; + let mut in_column_list = false; + let mut in_where_clause = false; for ancestor in name_ref.syntax().ancestors() { if ast::DropTable::can_cast(ancestor.kind()) { @@ -238,6 +259,18 @@ fn is_table_ref(name_ref: &ast::NameRef) -> bool { if ast::Table::can_cast(ancestor.kind()) { return true; } + if ast::ColumnList::can_cast(ancestor.kind()) { + in_column_list = true; + } + if ast::Insert::can_cast(ancestor.kind()) { + return !in_column_list; + } + if ast::WhereClause::can_cast(ancestor.kind()) { + in_where_clause = true; + } + if ast::Delete::can_cast(ancestor.kind()) { + return !in_where_clause; + } if ast::DropIndex::can_cast(ancestor.kind()) { return false; } @@ -297,6 +330,23 @@ fn is_select_from_table(name_ref: &ast::NameRef) -> bool { false } +fn is_select_column(name_ref: &ast::NameRef) -> bool { + let mut in_target_list = false; + + for ancestor in name_ref.syntax().ancestors() { + if ast::CallExpr::can_cast(ancestor.kind()) { + return false; + } + if ast::TargetList::can_cast(ancestor.kind()) { + in_target_list = true; + } + if ast::Select::can_cast(ancestor.kind()) && in_target_list { + return true; + } + } + false +} + fn hover_function( file: &ast::SourceFile, name_ref: &ast::NameRef, @@ -981,4 +1031,187 @@ select * from users$0; ╰╴ ─ hover "); } + + #[test] + fn hover_on_select_column() { + assert_snapshot!(check_hover(" +create table users(id int, email text); +select id$0 from users; +"), @r" + hover: public.users.id int + ╭▸ + 3 │ select id from users; + ╰╴ ─ hover + "); + } + + #[test] + fn hover_on_select_column_second() { + assert_snapshot!(check_hover(" +create table users(id int, email text); +select id, email$0 from users; +"), @r" + hover: public.users.email text + ╭▸ + 3 │ select id, email from users; + ╰╴ ─ hover + "); + } + + #[test] + fn hover_on_select_column_with_schema() { + assert_snapshot!(check_hover(" +create table public.users(id int, email text); +select email$0 from public.users; +"), @r" + hover: public.users.email text + ╭▸ + 3 │ select email from public.users; + ╰╴ ─ hover + "); + } + + #[test] + fn hover_on_select_column_with_search_path() { + assert_snapshot!(check_hover(" +set search_path to foo; +create table foo.users(id int, email text); +select id$0 from users; +"), @r" + hover: foo.users.id int + ╭▸ + 4 │ select id from users; + ╰╴ ─ hover + "); + } + + #[test] + fn hover_on_insert_table() { + assert_snapshot!(check_hover(" +create table users(id int, email text); +insert into users$0(id, email) values (1, 'test'); +"), @r" + hover: table public.users(id int, email text) + ╭▸ + 3 │ insert into users(id, email) values (1, 'test'); + ╰╴ ─ hover + "); + } + + #[test] + fn hover_on_insert_table_with_schema() { + assert_snapshot!(check_hover(" +create table public.users(id int, email text); +insert into public.users$0(id, email) values (1, 'test'); +"), @r" + hover: table public.users(id int, email text) + ╭▸ + 3 │ insert into public.users(id, email) values (1, 'test'); + ╰╴ ─ hover + "); + } + + #[test] + fn hover_on_insert_column() { + assert_snapshot!(check_hover(" +create table users(id int, email text); +insert into users(id$0, email) values (1, 'test'); +"), @r" + hover: public.users.id int + ╭▸ + 3 │ insert into users(id, email) values (1, 'test'); + ╰╴ ─ hover + "); + } + + #[test] + fn hover_on_insert_column_second() { + assert_snapshot!(check_hover(" +create table users(id int, email text); +insert into users(id, email$0) values (1, 'test'); +"), @r" + hover: public.users.email text + ╭▸ + 3 │ insert into users(id, email) values (1, 'test'); + ╰╴ ─ hover + "); + } + + #[test] + fn hover_on_insert_column_with_schema() { + assert_snapshot!(check_hover(" +create table public.users(id int, email text); +insert into public.users(email$0) values ('test'); +"), @r" + hover: public.users.email text + ╭▸ + 3 │ insert into public.users(email) values ('test'); + ╰╴ ─ hover + "); + } + + #[test] + fn hover_on_delete_table() { + assert_snapshot!(check_hover(" +create table users(id int, email text); +delete from users$0 where id = 1; +"), @r" + hover: table public.users(id int, email text) + ╭▸ + 3 │ delete from users where id = 1; + ╰╴ ─ hover + "); + } + + #[test] + fn hover_on_delete_table_with_schema() { + assert_snapshot!(check_hover(" +create table public.users(id int, email text); +delete from public.users$0 where id = 1; +"), @r" + hover: table public.users(id int, email text) + ╭▸ + 3 │ delete from public.users where id = 1; + ╰╴ ─ hover + "); + } + + #[test] + fn hover_on_delete_where_column() { + assert_snapshot!(check_hover(" +create table users(id int, email text); +delete from users where id$0 = 1; +"), @r" + hover: public.users.id int + ╭▸ + 3 │ delete from users where id = 1; + ╰╴ ─ hover + "); + } + + #[test] + fn hover_on_delete_where_column_second() { + assert_snapshot!(check_hover(" +create table users(id int, email text, active boolean); +delete from users where id = 1 and email$0 = 'test'; +"), @r" + hover: public.users.email text + ╭▸ + 3 │ delete from users where id = 1 and email = 'test'; + ╰╴ ─ hover + "); + } + + #[test] + fn hover_on_delete_where_column_with_schema() { + assert_snapshot!(check_hover(" +create table public.users(id int, email text); +delete from public.users where email$0 = 'test'; +"), @r" + hover: public.users.email text + ╭▸ + 3 │ delete from public.users where email = 'test'; + ╰╴ ─ hover + "); + } } diff --git a/crates/squawk_ide/src/resolve.rs b/crates/squawk_ide/src/resolve.rs index c518338b..25dc95bc 100644 --- a/crates/squawk_ide/src/resolve.rs +++ b/crates/squawk_ide/src/resolve.rs @@ -18,6 +18,7 @@ enum NameRefContext { CreateIndexColumn, SelectFunctionCall, SelectFromTable, + SelectColumn, InsertTable, InsertColumn, DeleteTable, @@ -84,6 +85,7 @@ pub(crate) fn resolve_name_ref(binder: &Binder, name_ref: &ast::NameRef) -> Opti resolve_function(binder, &function_name, &schema, position) } NameRefContext::CreateIndexColumn => resolve_create_index_column(binder, name_ref), + NameRefContext::SelectColumn => resolve_select_column(binder, name_ref), NameRefContext::InsertColumn => resolve_insert_column(binder, name_ref), NameRefContext::DeleteWhereColumn => resolve_delete_where_column(binder, name_ref), } @@ -95,6 +97,7 @@ fn classify_name_ref_context(name_ref: &ast::NameRef) -> Option let mut in_column_list = false; let mut in_where_clause = false; let mut in_from_clause = false; + let mut in_target_list = false; for ancestor in name_ref.syntax().ancestors() { if ast::DropTable::can_cast(ancestor.kind()) { @@ -124,6 +127,9 @@ fn classify_name_ref_context(name_ref: &ast::NameRef) -> Option if ast::FromClause::can_cast(ancestor.kind()) { in_from_clause = true; } + if ast::TargetList::can_cast(ancestor.kind()) { + in_target_list = true; + } if ast::Select::can_cast(ancestor.kind()) { if in_call_expr { return Some(NameRefContext::SelectFunctionCall); @@ -131,6 +137,9 @@ fn classify_name_ref_context(name_ref: &ast::NameRef) -> Option if in_from_clause { return Some(NameRefContext::SelectFromTable); } + if in_target_list { + return Some(NameRefContext::SelectColumn); + } } if ast::ColumnList::can_cast(ancestor.kind()) { in_column_list = true; @@ -287,6 +296,48 @@ fn resolve_insert_column(binder: &Binder, name_ref: &ast::NameRef) -> Option Option { + let column_name = Name::new(name_ref.syntax().text().to_string()); + + let select = name_ref.syntax().ancestors().find_map(ast::Select::cast)?; + let from_clause = select.from_clause()?; + let from_item = from_clause.from_items().next()?; + + let (table_name, schema) = if let Some(name_ref_node) = from_item.name_ref() { + (Name::new(name_ref_node.syntax().text().to_string()), None) + } else { + let field_expr = from_item.field_expr()?; + let table_name = Name::new(field_expr.field()?.syntax().text().to_string()); + let schema_name_ref = match field_expr.base()? { + ast::Expr::NameRef(name_ref) => name_ref, + _ => return None, + }; + let schema = Schema(Name::new(schema_name_ref.syntax().text().to_string())); + (table_name, Some(schema)) + }; + + let position = name_ref.syntax().text_range().start(); + let table_ptr = resolve_table(binder, &table_name, &schema, position)?; + + let root = &name_ref.syntax().ancestors().last()?; + let table_name_node = table_ptr.to_node(root); + let create_table = table_name_node + .ancestors() + .find_map(ast::CreateTable::cast)?; + let table_arg_list = create_table.table_arg_list()?; + + for arg in table_arg_list.args() { + if let ast::TableArg::Column(column) = arg + && let Some(col_name) = column.name() + && Name::new(col_name.syntax().text().to_string()) == column_name + { + return Some(SyntaxNodePtr::new(col_name.syntax())); + } + } + + None +} + fn resolve_delete_where_column(binder: &Binder, name_ref: &ast::NameRef) -> Option { let column_name = Name::new(name_ref.syntax().text().to_string()); From 108f3839f79b9c5dc799ab124e15cc00a98caa3f Mon Sep 17 00:00:00 2001 From: Steve Dignam Date: Sat, 27 Dec 2025 21:42:49 -0500 Subject: [PATCH 2/2] lint --- crates/squawk_ide/src/resolve.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/crates/squawk_ide/src/resolve.rs b/crates/squawk_ide/src/resolve.rs index 25dc95bc..8b01c61d 100644 --- a/crates/squawk_ide/src/resolve.rs +++ b/crates/squawk_ide/src/resolve.rs @@ -308,9 +308,8 @@ fn resolve_select_column(binder: &Binder, name_ref: &ast::NameRef) -> Option name_ref, - _ => return None, + let ast::Expr::NameRef(schema_name_ref) = field_expr.base()? else { + return None; }; let schema = Schema(Name::new(schema_name_ref.syntax().text().to_string())); (table_name, Some(schema))