From 845a9388a3e523729f4e9843192bc62fd1245b51 Mon Sep 17 00:00:00 2001 From: Steve Dignam Date: Mon, 2 Feb 2026 22:06:28 -0500 Subject: [PATCH] ide: on conflict goto def --- crates/squawk_ide/src/classify.rs | 23 +++- crates/squawk_ide/src/goto_definition.rs | 160 +++++++++++++++++++++++ crates/squawk_ide/src/resolve.rs | 23 ++-- 3 files changed, 190 insertions(+), 16 deletions(-) diff --git a/crates/squawk_ide/src/classify.rs b/crates/squawk_ide/src/classify.rs index 75d135a9..0b0ab935 100644 --- a/crates/squawk_ide/src/classify.rs +++ b/crates/squawk_ide/src/classify.rs @@ -107,6 +107,7 @@ pub(crate) fn classify_name_ref(name_ref: &ast::NameRef) -> Option let mut in_returning_clause = false; let mut in_when_clause = false; let mut in_special_sql_fn = false; + let mut in_conflict_target = false; // TODO: can we combine this if and the one that follows? if let Some(parent) = name_ref.syntax().parent() @@ -347,6 +348,7 @@ pub(crate) fn classify_name_ref(name_ref: &ast::NameRef) -> Option || ast::AlterTable::can_cast(ancestor.kind()) || ast::OnTable::can_cast(ancestor.kind()) || ast::AttachPartition::can_cast(ancestor.kind()) + || ast::DetachPartition::can_cast(ancestor.kind()) || ast::Table::can_cast(ancestor.kind()) || ast::Inherits::can_cast(ancestor.kind()) || ast::PartitionOf::can_cast(ancestor.kind()) @@ -379,7 +381,7 @@ pub(crate) fn classify_name_ref(name_ref: &ast::NameRef) -> Option return Some(NameRefClass::Database); } } - if ast::DropIndex::can_cast(ancestor.kind()) { + if ast::DropIndex::can_cast(ancestor.kind()) || ast::UsingIndex::can_cast(ancestor.kind()) { return Some(NameRefClass::Index); } if ast::DropType::can_cast(ancestor.kind()) || ast::DropDomain::can_cast(ancestor.kind()) { @@ -542,7 +544,7 @@ pub(crate) fn classify_name_ref(name_ref: &ast::NameRef) -> Option if ast::Call::can_cast(ancestor.kind()) { return Some(NameRefClass::CallProcedure); } - if ast::DropSchema::can_cast(ancestor.kind()) { + if ast::DropSchema::can_cast(ancestor.kind()) || ast::SetSchema::can_cast(ancestor.kind()) { return Some(NameRefClass::Schema); } if ast::CreateIndex::can_cast(ancestor.kind()) { @@ -625,8 +627,16 @@ pub(crate) fn classify_name_ref(name_ref: &ast::NameRef) -> Option if ast::PartitionItem::can_cast(ancestor.kind()) { in_partition_item = true; } + if ast::ConflictIndexItem::can_cast(ancestor.kind()) { + in_conflict_target = true; + } if ast::Insert::can_cast(ancestor.kind()) { - if in_returning_clause || in_column_list { + if in_returning_clause + || in_column_list + || in_set_clause + || in_where_clause + || in_conflict_target + { return Some(NameRefClass::InsertColumn); } return Some(NameRefClass::Table); @@ -640,10 +650,9 @@ pub(crate) fn classify_name_ref(name_ref: &ast::NameRef) -> Option if ast::SetClause::can_cast(ancestor.kind()) { in_set_clause = true; } - if ast::UsingClause::can_cast(ancestor.kind()) { - in_using_clause = true; - } - if ast::UsingOnClause::can_cast(ancestor.kind()) { + if ast::UsingClause::can_cast(ancestor.kind()) + || ast::UsingOnClause::can_cast(ancestor.kind()) + { in_using_clause = true; } if ast::ReturningClause::can_cast(ancestor.kind()) { diff --git a/crates/squawk_ide/src/goto_definition.rs b/crates/squawk_ide/src/goto_definition.rs index 1dd07035..4d168fdc 100644 --- a/crates/squawk_ide/src/goto_definition.rs +++ b/crates/squawk_ide/src/goto_definition.rs @@ -1402,6 +1402,31 @@ create table bar( "); } + #[test] + fn goto_alter_table_foreign_key_local_column() { + assert_snapshot!(goto(" +create table t ( + id bigserial primary key +); + +create table u ( + id bigserial primary key, + t_id bigint +); + +alter table u + add constraint fooo_fkey + foreign key (t_id$0) references t (id); +"), @r" + ╭▸ + 8 │ t_id bigint + │ ──── 2. destination + ‡ + 13 │ foreign key (t_id) references t (id); + ╰╴ ─ 1. source + "); + } + #[test] fn goto_check_constraint_column() { assert_snapshot!(goto(" @@ -6658,6 +6683,99 @@ alter table users$0 drop column email; "); } + #[test] + fn goto_alter_table_add_constraint_using_index() { + assert_snapshot!(goto(" +create table u(id int); +create index my_index on u (id); +alter table u add constraint uq unique using index my_in$0dex; +"), @r" + ╭▸ + 3 │ create index my_index on u (id); + │ ──────── 2. destination + 4 │ alter table u add constraint uq unique using index my_index; + ╰╴ ─ 1. source + "); + } + + #[test] + fn goto_alter_table_owner_to_role() { + assert_snapshot!(goto(" +create role reader; +create table t(id int); +alter table t owner to read$0er; +"), @r" + ╭▸ + 2 │ create role reader; + │ ────── 2. destination + 3 │ create table t(id int); + 4 │ alter table t owner to reader; + ╰╴ ─ 1. source + "); + } + + #[test] + fn goto_alter_table_set_tablespace() { + assert_snapshot!(goto(" +create tablespace ts location '/tmp/ts'; +create table t(id int); +alter table t set tablespace t$0s; +"), @r" + ╭▸ + 2 │ create tablespace ts location '/tmp/ts'; + │ ── 2. destination + 3 │ create table t(id int); + 4 │ alter table t set tablespace ts; + ╰╴ ─ 1. source + "); + } + + #[test] + fn goto_alter_table_set_schema() { + assert_snapshot!(goto(" +create schema foo; +create table t(id int); +alter table t set schema fo$0o; +"), @r" + ╭▸ + 2 │ create schema foo; + │ ─── 2. destination + 3 │ create table t(id int); + 4 │ alter table t set schema foo; + ╰╴ ─ 1. source + "); + } + + #[test] + fn goto_alter_table_attach_partition() { + assert_snapshot!(goto(" +create table parent (id int) partition by range (id); +create table child (id int); +alter table parent attach partition ch$0ild for values from (1) to (10); +"), @r" + ╭▸ + 3 │ create table child (id int); + │ ───── 2. destination + 4 │ alter table parent attach partition child for values from (1) to (10); + ╰╴ ─ 1. source + "); + } + + #[test] + fn goto_alter_table_detach_partition() { + assert_snapshot!(goto(" +create table parent (id int) partition by range (id); +create table child partition of parent for values from (1) to (10); +alter table parent detach partition ch$0ild; +"), @r" + ╭▸ + 3 │ create table child partition of parent for values from (1) to (10); + │ ───── 2. destination + 4 │ alter table parent detach partition child; + ╰╴ ─ 1. source + "); + } + #[test] fn goto_comment_on_table() { assert_snapshot!(goto(" @@ -7031,6 +7149,48 @@ insert into t as f values (1, 2) returning f.a$0;" "); } + #[test] + fn goto_insert_on_conflict_target_column() { + assert_snapshot!(goto(" +create table t(c text); +insert into t values ('c') on conflict (c$0) do nothing;" + ), @r" + ╭▸ + 2 │ create table t(c text); + │ ─ 2. destination + 3 │ insert into t values ('c') on conflict (c) do nothing; + ╰╴ ─ 1. source + "); + } + + #[test] + fn goto_insert_on_conflict_set_column() { + assert_snapshot!(goto(" +create table t(c text, d text); +insert into t values ('c', 'd') on conflict (c) do update set c$0 = excluded.c;" + ), @r" + ╭▸ + 2 │ create table t(c text, d text); + │ ─ 2. destination + 3 │ insert into t values ('c', 'd') on conflict (c) do update set c = excluded.c; + ╰╴ ─ 1. source + "); + } + + #[test] + fn goto_insert_on_conflict_excluded_column() { + assert_snapshot!(goto(" +create table t(c text, d text); +insert into t values ('c', 'd') on conflict (c) do update set c = excluded.c$0;" + ), @r" + ╭▸ + 2 │ create table t(c text, d text); + │ ─ 2. destination + 3 │ insert into t values ('c', 'd') on conflict (c) do update set c = excluded.c; + ╰╴ ─ 1. source + "); + } + #[test] fn goto_delete_from_alias() { assert_snapshot!(goto(" diff --git a/crates/squawk_ide/src/resolve.rs b/crates/squawk_ide/src/resolve.rs index a8474317..c971f690 100644 --- a/crates/squawk_ide/src/resolve.rs +++ b/crates/squawk_ide/src/resolve.rs @@ -87,10 +87,9 @@ pub(crate) fn resolve_name_ref_ptrs( resolve_view_name_ptr(binder, &table_name, &schema, position).map(|ptr| smallvec![ptr]) } NameRefClass::Index => { - let path = find_containing_path(name_ref)?; - let (index_name, schema) = extract_table_schema_from_path(&path)?; let position = name_ref.syntax().text_range().start(); - resolve_index_name_ptr(binder, &index_name, &schema, position).map(|ptr| smallvec![ptr]) + let index_name = Name::from_node(name_ref); + resolve_index_name_ptr(binder, &index_name, &None, position).map(|ptr| smallvec![ptr]) } NameRefClass::Type => { let (type_name, schema) = if let Some(parent) = name_ref.syntax().parent() @@ -220,13 +219,19 @@ pub(crate) fn resolve_name_ref_ptrs( resolve_column_for_path(binder, root, &path, column_name).map(|ptr| smallvec![ptr]) } NameRefClass::ConstraintColumn => { - let create_table = name_ref - .syntax() - .ancestors() - .find_map(ast::CreateTableLike::cast)?; let column_name = Name::from_node(name_ref); - find_column_in_create_table(binder, root, &create_table, &column_name) - .map(|ptr| smallvec![ptr]) + for ancestor in name_ref.syntax().ancestors() { + if let Some(create_table) = ast::CreateTableLike::cast(ancestor.clone()) { + return find_column_in_create_table(binder, root, &create_table, &column_name) + .map(|ptr| smallvec![ptr]); + } + if let Some(alter_table) = ast::AlterTable::cast(ancestor) { + let table_path = alter_table.relation_name()?.path()?; + return resolve_column_for_path(binder, root, &table_path, column_name) + .map(|ptr| smallvec![ptr]); + } + } + None } NameRefClass::PolicyColumn => { let on_table_path = name_ref.syntax().ancestors().find_map(|n| {