From 95da3e962d6480dc4c3760aa1f84eb0b434d48d3 Mon Sep 17 00:00:00 2001 From: Steve Dignam Date: Wed, 14 Jan 2026 23:29:42 -0500 Subject: [PATCH] ide: goto def with create/drop trigger --- crates/squawk_ide/src/binder.rs | 78 ++++++++++++++++++ crates/squawk_ide/src/classify.rs | 26 ++++++ crates/squawk_ide/src/goto_definition.rs | 100 +++++++++++++++++++++++ crates/squawk_ide/src/hover.rs | 45 +++++++++- crates/squawk_ide/src/resolve.rs | 48 ++++++++++- crates/squawk_ide/src/symbols.rs | 2 + 6 files changed, 296 insertions(+), 3 deletions(-) diff --git a/crates/squawk_ide/src/binder.rs b/crates/squawk_ide/src/binder.rs index 74779101..e6f04129 100644 --- a/crates/squawk_ide/src/binder.rs +++ b/crates/squawk_ide/src/binder.rs @@ -107,6 +107,34 @@ impl Binder { None } + pub(crate) fn lookup_with_table( + &self, + name: &Name, + kind: SymbolKind, + position: TextSize, + schema: &Option, + table: &Option, + ) -> Option { + let symbols = self.scopes[self.root_scope()].get(name)?; + + let search_paths = match schema { + Some(s) => std::slice::from_ref(s), + None => self.search_path_at(position), + }; + + for search_schema in search_paths { + if let Some(symbol_id) = symbols.iter().copied().find(|id| { + let symbol = &self.symbols[*id]; + symbol.kind == kind + && symbol.schema.as_ref() == Some(search_schema) + && &symbol.table == table + }) { + return Some(self.symbols[symbol_id].ptr); + } + } + None + } + pub(crate) fn lookup_info( &self, name_str: String, @@ -186,6 +214,7 @@ fn bind_stmt(b: &mut Binder, stmt: ast::Stmt) { bind_create_materialized_view(b, create_view) } ast::Stmt::CreateSequence(create_sequence) => bind_create_sequence(b, create_sequence), + ast::Stmt::CreateTrigger(create_trigger) => bind_create_trigger(b, create_trigger), ast::Stmt::CreateTablespace(create_tablespace) => { bind_create_tablespace(b, create_tablespace) } @@ -217,6 +246,7 @@ fn bind_create_table(b: &mut Binder, create_table: impl ast::HasCreateTable) { ptr: name_ptr, schema: Some(schema.clone()), params: None, + table: None, }); let type_id = b.symbols.alloc(Symbol { @@ -224,6 +254,7 @@ fn bind_create_table(b: &mut Binder, create_table: impl ast::HasCreateTable) { ptr: name_ptr, schema: Some(schema), params: None, + table: None, }); let root = b.root_scope(); @@ -248,6 +279,7 @@ fn bind_create_index(b: &mut Binder, create_index: ast::CreateIndex) { ptr: name_ptr, schema: Some(schema), params: None, + table: None, }); let root = b.root_scope(); @@ -276,6 +308,7 @@ fn bind_create_function(b: &mut Binder, create_function: ast::CreateFunction) { ptr: name_ptr, schema: Some(schema), params, + table: None, }); let root = b.root_scope(); @@ -304,6 +337,7 @@ fn bind_create_aggregate(b: &mut Binder, create_aggregate: ast::CreateAggregate) ptr: name_ptr, schema: Some(schema), params, + table: None, }); let root = b.root_scope(); @@ -332,6 +366,7 @@ fn bind_create_procedure(b: &mut Binder, create_procedure: ast::CreateProcedure) ptr: name_ptr, schema: Some(schema), params, + table: None, }); let root = b.root_scope(); @@ -360,6 +395,7 @@ fn bind_create_schema(b: &mut Binder, create_schema: ast::CreateSchema) { ptr: name_ptr, schema: Some(Schema(schema_name.clone())), params: None, + table: None, }); let root = b.root_scope(); @@ -386,6 +422,7 @@ fn bind_create_type(b: &mut Binder, create_type: ast::CreateType) { ptr: name_ptr, schema: Some(schema), params: None, + table: None, }); let root = b.root_scope(); @@ -413,6 +450,7 @@ fn bind_create_view(b: &mut Binder, create_view: ast::CreateView) { ptr: name_ptr, schema: Some(schema), params: None, + table: None, }); let root = b.root_scope(); @@ -440,6 +478,7 @@ fn bind_create_materialized_view(b: &mut Binder, create_view: ast::CreateMateria ptr: name_ptr, schema: Some(schema), params: None, + table: None, }); let root = b.root_scope(); @@ -468,12 +507,45 @@ fn bind_create_sequence(b: &mut Binder, create_sequence: ast::CreateSequence) { ptr: name_ptr, schema: Some(schema), params: None, + table: None, }); let root = b.root_scope(); b.scopes[root].insert(sequence_name, sequence_id); } +fn bind_create_trigger(b: &mut Binder, create_trigger: ast::CreateTrigger) { + let Some(name) = create_trigger.name() else { + return; + }; + + let trigger_name = Name::from_node(&name); + let name_ptr = SyntaxNodePtr::new(name.syntax()); + + let Some(table_path) = create_trigger.on_table().and_then(|on| on.path()) else { + return; + }; + + let Some(table_name) = item_name(&table_path) else { + return; + }; + + let Some(schema) = schema_name(b, &table_path, false) else { + return; + }; + + let trigger_id = b.symbols.alloc(Symbol { + kind: SymbolKind::Trigger, + ptr: name_ptr, + schema: Some(schema), + params: None, + table: Some(table_name), + }); + + let root = b.root_scope(); + b.scopes[root].insert(trigger_name, trigger_id); +} + fn bind_create_tablespace(b: &mut Binder, create_tablespace: ast::CreateTablespace) { let Some(name) = create_tablespace.name() else { return; @@ -487,6 +559,7 @@ fn bind_create_tablespace(b: &mut Binder, create_tablespace: ast::CreateTablespa ptr: name_ptr, schema: None, params: None, + table: None, }); let root = b.root_scope(); @@ -506,6 +579,7 @@ fn bind_create_database(b: &mut Binder, create_database: ast::CreateDatabase) { ptr: name_ptr, schema: None, params: None, + table: None, }); let root = b.root_scope(); @@ -525,6 +599,7 @@ fn bind_create_server(b: &mut Binder, create_server: ast::CreateServer) { ptr: name_ptr, schema: None, params: None, + table: None, }); let root = b.root_scope(); @@ -544,6 +619,7 @@ fn bind_create_extension(b: &mut Binder, create_extension: ast::CreateExtension) ptr: name_ptr, schema: None, params: None, + table: None, }); let root = b.root_scope(); @@ -563,6 +639,7 @@ fn bind_declare_cursor(b: &mut Binder, declare: ast::Declare) { ptr: name_ptr, schema: None, params: None, + table: None, }); let root = b.root_scope(); @@ -582,6 +659,7 @@ fn bind_prepare(b: &mut Binder, prepare: ast::Prepare) { ptr: name_ptr, schema: None, params: None, + table: None, }); let root = b.root_scope(); diff --git a/crates/squawk_ide/src/classify.rs b/crates/squawk_ide/src/classify.rs index cc81a5e1..9f90e66e 100644 --- a/crates/squawk_ide/src/classify.rs +++ b/crates/squawk_ide/src/classify.rs @@ -6,12 +6,14 @@ use squawk_syntax::{ #[derive(Debug)] pub(crate) enum NameRefClass { DropTable, + DropForeignTable, Table, DropIndex, DropType, DropView, DropMaterializedView, DropSequence, + DropTrigger, SequenceOwnedByColumn, Tablespace, DropDatabase, @@ -94,6 +96,8 @@ pub(crate) enum NameRefClass { NamedArgParameter, Cursor, PreparedStatement, + TriggerFunctionCall, + TriggerProcedureCall, } fn is_special_fn(kind: SyntaxKind) -> bool { @@ -369,6 +373,9 @@ pub(crate) fn classify_name_ref(name_ref: &ast::NameRef) -> Option if ast::DropTable::can_cast(ancestor.kind()) { return Some(NameRefClass::DropTable); } + if ast::DropForeignTable::can_cast(ancestor.kind()) { + return Some(NameRefClass::DropForeignTable); + } if ast::Truncate::can_cast(ancestor.kind()) { return Some(NameRefClass::TruncateTable); } @@ -387,6 +394,9 @@ pub(crate) fn classify_name_ref(name_ref: &ast::NameRef) -> Option if ast::AlterTable::can_cast(ancestor.kind()) { return Some(NameRefClass::AlterTable); } + if ast::OnTable::can_cast(ancestor.kind()) { + return Some(NameRefClass::Table); + } if ast::AttachPartition::can_cast(ancestor.kind()) { return Some(NameRefClass::AttachPartition); } @@ -428,6 +438,9 @@ pub(crate) fn classify_name_ref(name_ref: &ast::NameRef) -> Option if ast::DropSequence::can_cast(ancestor.kind()) { return Some(NameRefClass::DropSequence); } + if ast::DropTrigger::can_cast(ancestor.kind()) { + return Some(NameRefClass::DropTrigger); + } if ast::DropDatabase::can_cast(ancestor.kind()) { return Some(NameRefClass::DropDatabase); } @@ -573,6 +586,15 @@ pub(crate) fn classify_name_ref(name_ref: &ast::NameRef) -> Option } return Some(NameRefClass::CreateIndex); } + if let Some(create_trigger) = ast::CreateTrigger::cast(ancestor.clone()) + && in_call_expr + && !in_arg_list + { + if create_trigger.procedure_token().is_some() { + return Some(NameRefClass::TriggerProcedureCall); + } + return Some(NameRefClass::TriggerFunctionCall); + } if in_partition_item && ast::CreateTableLike::can_cast(ancestor.kind()) { return Some(NameRefClass::PartitionByColumn); } @@ -713,6 +735,7 @@ pub(crate) enum NameClass { WithTable(ast::WithTable), CreateIndex(ast::CreateIndex), CreateSequence(ast::CreateSequence), + CreateTrigger(ast::CreateTrigger), CreateTablespace(ast::CreateTablespace), CreateDatabase(ast::CreateDatabase), CreateServer(ast::CreateServer), @@ -756,6 +779,9 @@ pub(crate) fn classify_name(name: &ast::Name) -> Option { if let Some(create_sequence) = ast::CreateSequence::cast(ancestor.clone()) { return Some(NameClass::CreateSequence(create_sequence)); } + if let Some(create_trigger) = ast::CreateTrigger::cast(ancestor.clone()) { + return Some(NameClass::CreateTrigger(create_trigger)); + } if let Some(create_tablespace) = ast::CreateTablespace::cast(ancestor.clone()) { return Some(NameClass::CreateTablespace(create_tablespace)); } diff --git a/crates/squawk_ide/src/goto_definition.rs b/crates/squawk_ide/src/goto_definition.rs index c13c556d..4469c091 100644 --- a/crates/squawk_ide/src/goto_definition.rs +++ b/crates/squawk_ide/src/goto_definition.rs @@ -244,6 +244,20 @@ drop table t$0; "); } + #[test] + fn goto_drop_foreign_table() { + assert_snapshot!(goto(" +create foreign table t(a int) server s; +drop foreign table t$0; +"), @r" + ╭▸ + 2 │ create foreign table t(a int) server s; + │ ─ 2. destination + 3 │ drop foreign table t; + ╰╴ ─ 1. source + "); + } + #[test] fn goto_definition_prefers_previous_token() { assert_snapshot!(goto(" @@ -428,6 +442,92 @@ drop sequence s$0; "); } + #[test] + fn goto_drop_trigger() { + assert_snapshot!(goto(" +create trigger tr before insert on t for each row execute function f(); +drop trigger tr$0 on t; +"), @r" + ╭▸ + 2 │ create trigger tr before insert on t for each row execute function f(); + │ ── 2. destination + 3 │ drop trigger tr on t; + ╰╴ ─ 1. source + "); + } + + #[test] + fn goto_create_trigger_function() { + assert_snapshot!(goto(" +create function f() returns trigger as 'select 1' language sql; +create trigger tr before insert on t for each row execute function f$0(); +"), @r" + ╭▸ + 2 │ create function f() returns trigger as 'select 1' language sql; + │ ─ 2. destination + 3 │ create trigger tr before insert on t for each row execute function f(); + ╰╴ ─ 1. source + "); + } + + #[test] + fn goto_create_trigger_procedure() { + assert_snapshot!(goto(" +create procedure a() language sql as 'select 1'; +create trigger tr before truncate or delete or insert +on t +execute procedure a$0(); +"), @r" + ╭▸ + 2 │ create procedure a() language sql as 'select 1'; + │ ─ 2. destination + ‡ + 5 │ execute procedure a(); + ╰╴ ─ 1. source + "); + } + + #[test] + fn goto_drop_trigger_table_specific() { + assert_snapshot!(goto(" +create table u(a int); +create trigger tr before truncate +on u +execute function noop(); + +create table t(b int); +create trigger tr before truncate +on t +execute function noop(); + +drop trigger tr$0 on t; +"), @r" + ╭▸ + 8 │ create trigger tr before truncate + │ ── 2. destination + ‡ + 12 │ drop trigger tr on t; + ╰╴ ─ 1. source + "); + } + + #[test] + fn goto_create_trigger_table() { + assert_snapshot!(goto(" +create table t(b int); +create trigger tr before truncate +on t$0 +execute function noop(); +"), @r" + ╭▸ + 2 │ create table t(b int); + │ ─ 2. destination + 3 │ create trigger tr before truncate + 4 │ on t + ╰╴ ─ 1. source + "); + } + #[test] fn goto_create_sequence_owned_by() { assert_snapshot!(goto(" diff --git a/crates/squawk_ide/src/hover.rs b/crates/squawk_ide/src/hover.rs index 9c0ebed2..f6954498 100644 --- a/crates/squawk_ide/src/hover.rs +++ b/crates/squawk_ide/src/hover.rs @@ -91,6 +91,7 @@ pub fn hover(file: &ast::SourceFile, offset: TextSize) -> Option { } NameRefClass::Table | NameRefClass::DropTable + | NameRefClass::DropForeignTable | NameRefClass::DropView | NameRefClass::DropMaterializedView | NameRefClass::CreateIndex @@ -125,6 +126,7 @@ pub fn hover(file: &ast::SourceFile, offset: TextSize) -> Option { return hover_table(root, &name_ref, &binder); } NameRefClass::DropSequence => return hover_sequence(root, &name_ref, &binder), + NameRefClass::DropTrigger => return hover_trigger(root, &name_ref, &binder), NameRefClass::DropDatabase | NameRefClass::ReindexDatabase | NameRefClass::ReindexSystem => return hover_database(root, &name_ref, &binder), @@ -141,11 +143,15 @@ pub fn hover(file: &ast::SourceFile, offset: TextSize) -> Option { NameRefClass::DropIndex | NameRefClass::ReindexIndex => { return hover_index(root, &name_ref, &binder); } - NameRefClass::DropFunction | NameRefClass::DefaultConstraintFunctionCall => { + NameRefClass::DropFunction + | NameRefClass::DefaultConstraintFunctionCall + | NameRefClass::TriggerFunctionCall => { return hover_function(root, &name_ref, &binder); } NameRefClass::DropAggregate => return hover_aggregate(root, &name_ref, &binder), - NameRefClass::DropProcedure | NameRefClass::CallProcedure => { + NameRefClass::DropProcedure + | NameRefClass::CallProcedure + | NameRefClass::TriggerProcedureCall => { return hover_procedure(root, &name_ref, &binder); } NameRefClass::DropRoutine => return hover_routine(root, &name_ref, &binder), @@ -192,6 +198,9 @@ pub fn hover(file: &ast::SourceFile, offset: TextSize) -> Option { NameClass::CreateSequence(create_sequence) => { return format_create_sequence(&create_sequence, &binder); } + NameClass::CreateTrigger(create_trigger) => { + return format_create_trigger(&create_trigger, &binder); + } NameClass::CreateTablespace(create_tablespace) => { return format_create_tablespace(&create_tablespace); } @@ -739,6 +748,24 @@ fn hover_sequence( format_create_sequence(&create_sequence, binder) } +fn hover_trigger( + root: &SyntaxNode, + name_ref: &ast::NameRef, + binder: &binder::Binder, +) -> Option { + let trigger_ptr = resolve::resolve_name_ref(binder, root, name_ref)? + .into_iter() + .next()?; + + let trigger_name_node = trigger_ptr.to_node(root); + + let create_trigger = trigger_name_node + .ancestors() + .find_map(ast::CreateTrigger::cast)?; + + format_create_trigger(&create_trigger, binder) +} + fn hover_tablespace( root: &SyntaxNode, name_ref: &ast::NameRef, @@ -960,6 +987,20 @@ fn format_create_sequence( Some(format!("sequence {}.{}", schema, sequence_name)) } +fn format_create_trigger( + create_trigger: &ast::CreateTrigger, + binder: &binder::Binder, +) -> Option { + let trigger_name = create_trigger.name()?.syntax().text().to_string(); + let on_table_path = create_trigger.on_table()?.path()?; + + let (schema, table_name) = resolve::resolve_table_info(binder, &on_table_path)?; + Some(format!( + "trigger {}.{} on {}.{}", + schema, trigger_name, schema, table_name + )) +} + fn format_create_tablespace(create_tablespace: &ast::CreateTablespace) -> Option { let name = create_tablespace.name()?.syntax().text().to_string(); Some(format!("tablespace {}", name)) diff --git a/crates/squawk_ide/src/resolve.rs b/crates/squawk_ide/src/resolve.rs index 6727712d..9f5960de 100644 --- a/crates/squawk_ide/src/resolve.rs +++ b/crates/squawk_ide/src/resolve.rs @@ -20,6 +20,7 @@ pub(crate) fn resolve_name_ref( match context { NameRefClass::DropTable + | NameRefClass::DropForeignTable | NameRefClass::Table | NameRefClass::CreateIndex | NameRefClass::InsertTable @@ -148,6 +149,25 @@ pub(crate) fn resolve_name_ref( resolve_sequence_name_ptr(binder, &sequence_name, &schema, position) .map(|ptr| smallvec![ptr]) } + NameRefClass::DropTrigger => { + let drop_trigger = name_ref + .syntax() + .ancestors() + .find_map(ast::DropTrigger::cast)?; + let path = drop_trigger.path()?; + let trigger_name = extract_table_name(&path)?; + let mut schema = extract_schema_name(&path); + let on_table_path = drop_trigger + .on_table() + .and_then(|on_table| on_table.path())?; + if schema.is_none() { + schema = extract_schema_name(&on_table_path); + } + let table_name = extract_table_name(&on_table_path)?; + let position = name_ref.syntax().text_range().start(); + resolve_trigger_name_ptr(binder, &trigger_name, &schema, position, Some(table_name)) + .map(|ptr| smallvec![ptr]) + } NameRefClass::ReindexDatabase | NameRefClass::ReindexSystem | NameRefClass::DropDatabase => { @@ -324,7 +344,7 @@ pub(crate) fn resolve_name_ref( let schema_name = Name::from_node(name_ref); resolve_schema(binder, &schema_name).map(|ptr| smallvec![ptr]) } - NameRefClass::DefaultConstraintFunctionCall => { + NameRefClass::DefaultConstraintFunctionCall | NameRefClass::TriggerFunctionCall => { let schema = if let Some(parent_node) = name_ref.syntax().parent() && let Some(field_expr) = ast::FieldExpr::cast(parent_node) { @@ -339,6 +359,22 @@ pub(crate) fn resolve_name_ref( resolve_function(binder, &function_name, &schema, None, position) .map(|ptr| smallvec![ptr]) } + NameRefClass::TriggerProcedureCall => { + let schema = if let Some(parent_node) = name_ref.syntax().parent() + && let Some(field_expr) = ast::FieldExpr::cast(parent_node) + { + let base = field_expr.base()?; + let schema_name_ref = ast::NameRef::cast(base.syntax().clone())?; + Some(Schema(Name::from_node(&schema_name_ref))) + } else { + None + }; + let procedure_name = Name::from_node(name_ref); + let position = name_ref.syntax().text_range().start(); + + resolve_procedure(binder, &procedure_name, &schema, None, position) + .map(|ptr| smallvec![ptr]) + } NameRefClass::SelectFunctionCall => { let schema = if let Some(parent_node) = name_ref.syntax().parent() && let Some(field_expr) = ast::FieldExpr::cast(parent_node) @@ -492,6 +528,16 @@ fn resolve_sequence_name_ptr( binder.lookup_with(sequence_name, SymbolKind::Sequence, position, schema) } +fn resolve_trigger_name_ptr( + binder: &Binder, + trigger_name: &Name, + schema: &Option, + position: TextSize, + table: Option, +) -> Option { + binder.lookup_with_table(trigger_name, SymbolKind::Trigger, position, schema, &table) +} + fn resolve_tablespace_name_ptr(binder: &Binder, tablespace_name: &Name) -> Option { binder.lookup(tablespace_name, SymbolKind::Tablespace) } diff --git a/crates/squawk_ide/src/symbols.rs b/crates/squawk_ide/src/symbols.rs index f0ae920a..93212d71 100644 --- a/crates/squawk_ide/src/symbols.rs +++ b/crates/squawk_ide/src/symbols.rs @@ -59,6 +59,7 @@ pub(crate) enum SymbolKind { Database, Server, Extension, + Trigger, } #[derive(Clone, Debug)] @@ -67,6 +68,7 @@ pub(crate) struct Symbol { pub(crate) ptr: SyntaxNodePtr, pub(crate) schema: Option, pub(crate) params: Option>, + pub(crate) table: Option, } pub(crate) type SymbolId = Idx;