diff --git a/crates/squawk_ide/src/binder.rs b/crates/squawk_ide/src/binder.rs index 26ee7a94..543ffa4e 100644 --- a/crates/squawk_ide/src/binder.rs +++ b/crates/squawk_ide/src/binder.rs @@ -224,6 +224,7 @@ fn bind_stmt(b: &mut Binder, stmt: ast::Stmt) { ast::Stmt::CreateDatabase(create_database) => bind_create_database(b, create_database), ast::Stmt::CreateServer(create_server) => bind_create_server(b, create_server), ast::Stmt::CreateExtension(create_extension) => bind_create_extension(b, create_extension), + ast::Stmt::CreateRole(create_role) => bind_create_role(b, create_role), ast::Stmt::Declare(declare) => bind_declare_cursor(b, declare), ast::Stmt::Prepare(prepare) => bind_prepare(b, prepare), ast::Stmt::Listen(listen) => bind_listen(b, listen), @@ -646,6 +647,26 @@ fn bind_create_extension(b: &mut Binder, create_extension: ast::CreateExtension) b.scopes[root].insert(extension_name, extension_id); } +fn bind_create_role(b: &mut Binder, create_role: ast::CreateRole) { + let Some(name) = create_role.name() else { + return; + }; + + let role_name = Name::from_node(&name); + let name_ptr = SyntaxNodePtr::new(name.syntax()); + + let role_id = b.symbols.alloc(Symbol { + kind: SymbolKind::Role, + ptr: name_ptr, + schema: None, + params: None, + table: None, + }); + + let root = b.root_scope(); + b.scopes[root].insert(role_name, role_id); +} + fn bind_declare_cursor(b: &mut Binder, declare: ast::Declare) { let Some(name) = declare.name() else { return; diff --git a/crates/squawk_ide/src/classify.rs b/crates/squawk_ide/src/classify.rs index cbd54ad0..f53d219b 100644 --- a/crates/squawk_ide/src/classify.rs +++ b/crates/squawk_ide/src/classify.rs @@ -24,6 +24,10 @@ pub(crate) enum NameRefClass { DropExtension, AlterExtension, ForeignTableServerName, + AlterRole, + DropRole, + SetRole, + Role, ForeignKeyTable, ForeignKeyColumn, ForeignKeyLocalColumn, @@ -470,6 +474,18 @@ pub(crate) fn classify_name_ref(name_ref: &ast::NameRef) -> Option if ast::AlterExtension::can_cast(ancestor.kind()) { return Some(NameRefClass::AlterExtension); } + if ast::AlterRole::can_cast(ancestor.kind()) { + return Some(NameRefClass::AlterRole); + } + if ast::DropRole::can_cast(ancestor.kind()) { + return Some(NameRefClass::DropRole); + } + if ast::SetRole::can_cast(ancestor.kind()) { + return Some(NameRefClass::SetRole); + } + if ast::RoleRef::can_cast(ancestor.kind()) { + return Some(NameRefClass::Role); + } if ast::ServerName::can_cast(ancestor.kind()) { return Some(NameRefClass::ForeignTableServerName); } @@ -755,6 +771,7 @@ pub(crate) enum NameClass { CreateDatabase(ast::CreateDatabase), CreateServer(ast::CreateServer), CreateExtension(ast::CreateExtension), + CreateRole(ast::CreateRole), CreateType(ast::CreateType), CreateFunction(ast::CreateFunction), CreateAggregate(ast::CreateAggregate), @@ -813,6 +830,9 @@ pub(crate) fn classify_name(name: &ast::Name) -> Option { if let Some(create_extension) = ast::CreateExtension::cast(ancestor.clone()) { return Some(NameClass::CreateExtension(create_extension)); } + if let Some(create_role) = ast::CreateRole::cast(ancestor.clone()) { + return Some(NameClass::CreateRole(create_role)); + } if let Some(create_type) = ast::CreateType::cast(ancestor.clone()) { return Some(NameClass::CreateType(create_type)); } diff --git a/crates/squawk_ide/src/document_symbols.rs b/crates/squawk_ide/src/document_symbols.rs index 9d407010..889aadb1 100644 --- a/crates/squawk_ide/src/document_symbols.rs +++ b/crates/squawk_ide/src/document_symbols.rs @@ -17,6 +17,7 @@ pub enum DocumentSymbolKind { Aggregate, Procedure, EventTrigger, + Role, Type, Enum, Column, @@ -75,6 +76,11 @@ pub fn document_symbols(file: &ast::SourceFile) -> Vec { symbols.push(symbol); } } + ast::Stmt::CreateRole(create_role) => { + if let Some(symbol) = create_role_symbol(create_role) { + symbols.push(symbol); + } + } ast::Stmt::CreateType(create_type) => { if let Some(symbol) = create_type_symbol(&binder, create_type) { symbols.push(symbol); @@ -388,6 +394,23 @@ fn create_event_trigger_symbol( }) } +fn create_role_symbol(create_role: ast::CreateRole) -> Option { + let name_node = create_role.name()?; + let name = name_node.syntax().text().to_string(); + + let full_range = create_role.syntax().text_range(); + let focus_range = name_node.syntax().text_range(); + + Some(DocumentSymbol { + name, + detail: None, + kind: DocumentSymbolKind::Role, + full_range, + focus_range, + children: vec![], + }) +} + fn create_type_symbol( binder: &binder::Binder, create_type: ast::CreateType, @@ -598,6 +621,7 @@ mod tests { DocumentSymbolKind::Aggregate => "aggregate", DocumentSymbolKind::Procedure => "procedure", DocumentSymbolKind::EventTrigger => "event trigger", + DocumentSymbolKind::Role => "role", DocumentSymbolKind::Type => "type", DocumentSymbolKind::Enum => "enum", DocumentSymbolKind::Column => "column", @@ -843,6 +867,21 @@ unlisten *; ); } + #[test] + fn create_role() { + assert_snapshot!(symbols(" +create role reader; +"), @r" + info: role: reader + ╭▸ + 2 │ create role reader; + │ ┬───────────┯━━━━━ + │ │ │ + │ │ focus range + ╰╴full range + "); + } + #[test] fn multiple_symbols() { assert_snapshot!(symbols(" diff --git a/crates/squawk_ide/src/goto_definition.rs b/crates/squawk_ide/src/goto_definition.rs index ce12fe07..e9a204a6 100644 --- a/crates/squawk_ide/src/goto_definition.rs +++ b/crates/squawk_ide/src/goto_definition.rs @@ -673,6 +673,76 @@ drop database my$0db; "); } + #[test] + fn goto_drop_role() { + assert_snapshot!(goto(" +create role reader; +drop role read$0er; +"), @r" + ╭▸ + 2 │ create role reader; + │ ────── 2. destination + 3 │ drop role reader; + ╰╴ ─ 1. source + "); + } + + #[test] + fn goto_alter_role() { + assert_snapshot!(goto(" +create role reader; +alter role read$0er rename to writer; +"), @r" + ╭▸ + 2 │ create role reader; + │ ────── 2. destination + 3 │ alter role reader rename to writer; + ╰╴ ─ 1. source + "); + } + + #[test] + fn goto_set_role() { + assert_snapshot!(goto(" +create role reader; +set role read$0er; +"), @r" + ╭▸ + 2 │ create role reader; + │ ────── 2. destination + 3 │ set role reader; + ╰╴ ─ 1. source + "); + } + + #[test] + fn goto_create_tablespace_owner_role() { + assert_snapshot!(goto(" +create role reader; +create tablespace t owner read$0er location 'foo'; +"), @r" + ╭▸ + 2 │ create role reader; + │ ────── 2. destination + 3 │ create tablespace t owner reader location 'foo'; + ╰╴ ─ 1. source + "); + } + + #[test] + fn goto_role_definition_returns_self() { + assert_snapshot!(goto(" +create role read$0er; +"), @r" + ╭▸ + 2 │ create role reader; + │ ┬──┬── + │ │ │ + │ │ 1. source + ╰╴ 2. destination + "); + } + #[test] fn goto_drop_database_defined_after() { assert_snapshot!(goto(" diff --git a/crates/squawk_ide/src/hover.rs b/crates/squawk_ide/src/hover.rs index f80ae120..083a52e8 100644 --- a/crates/squawk_ide/src/hover.rs +++ b/crates/squawk_ide/src/hover.rs @@ -142,6 +142,12 @@ pub fn hover(file: &ast::SourceFile, offset: TextSize) -> Option { NameRefClass::DropExtension | NameRefClass::AlterExtension => { return hover_extension(root, &name_ref, &binder); } + NameRefClass::AlterRole + | NameRefClass::DropRole + | NameRefClass::SetRole + | NameRefClass::Role => { + return hover_role(root, &name_ref, &binder); + } NameRefClass::Tablespace => return hover_tablespace(root, &name_ref, &binder), NameRefClass::DropIndex | NameRefClass::ReindexIndex => { return hover_index(root, &name_ref, &binder); @@ -221,6 +227,9 @@ pub fn hover(file: &ast::SourceFile, offset: TextSize) -> Option { NameClass::CreateExtension(create_extension) => { return format_create_extension(&create_extension); } + NameClass::CreateRole(create_role) => { + return format_create_role(&create_role); + } NameClass::CreateType(create_type) => { return format_create_type(&create_type, &binder); } @@ -843,6 +852,18 @@ fn hover_extension( Some(format!("extension {}", extension_name_node.text())) } +fn hover_role( + root: &SyntaxNode, + name_ref: &ast::NameRef, + binder: &binder::Binder, +) -> Option { + let role_ptr = resolve::resolve_name_ref(binder, root, name_ref)? + .into_iter() + .next()?; + let role_name_node = role_ptr.to_node(root); + Some(format!("role {}", role_name_node.text())) +} + fn hover_cursor( root: &SyntaxNode, name_ref: &ast::NameRef, @@ -1073,6 +1094,11 @@ fn format_create_extension(create_extension: &ast::CreateExtension) -> Option Option { + let name = create_role.name()?.syntax().text().to_string(); + Some(format!("role {}", name)) +} + fn index_schema(create_index: &ast::CreateIndex, binder: &binder::Binder) -> Option { let position = create_index.syntax().text_range().start(); let search_path = binder.search_path_at(position); @@ -4098,6 +4124,70 @@ alter extension my$0ext update to '2.0'; "); } + #[test] + fn hover_role_on_create() { + assert_snapshot!(check_hover(" +create role read$0er; +"), @r" + hover: role reader + ╭▸ + 2 │ create role reader; + ╰╴ ─ hover + "); + } + + #[test] + fn hover_role_on_alter() { + assert_snapshot!(check_hover(" +create role reader; +alter role read$0er rename to writer; +"), @r" + hover: role reader + ╭▸ + 3 │ alter role reader rename to writer; + ╰╴ ─ hover + "); + } + + #[test] + fn hover_role_on_drop() { + assert_snapshot!(check_hover(" +create role reader; +drop role read$0er; +"), @r" + hover: role reader + ╭▸ + 3 │ drop role reader; + ╰╴ ─ hover + "); + } + + #[test] + fn hover_role_on_set() { + assert_snapshot!(check_hover(" +create role reader; +set role read$0er; +"), @r" + hover: role reader + ╭▸ + 3 │ set role reader; + ╰╴ ─ hover + "); + } + + #[test] + fn hover_role_on_create_tablespace_owner() { + assert_snapshot!(check_hover(" +create role reader; +create tablespace t owner read$0er location 'foo'; +"), @r" + hover: role reader + ╭▸ + 3 │ create tablespace t owner reader location 'foo'; + ╰╴ ─ hover + "); + } + #[test] fn hover_on_fetch_cursor() { assert_snapshot!(check_hover(" diff --git a/crates/squawk_ide/src/resolve.rs b/crates/squawk_ide/src/resolve.rs index db4f9409..a8328351 100644 --- a/crates/squawk_ide/src/resolve.rs +++ b/crates/squawk_ide/src/resolve.rs @@ -193,6 +193,13 @@ pub(crate) fn resolve_name_ref( let extension_name = Name::from_node(name_ref); resolve_extension_name_ptr(binder, &extension_name).map(|ptr| smallvec![ptr]) } + NameRefClass::AlterRole + | NameRefClass::DropRole + | NameRefClass::SetRole + | NameRefClass::Role => { + let role_name = Name::from_node(name_ref); + resolve_role_name_ptr(binder, &role_name).map(|ptr| smallvec![ptr]) + } NameRefClass::SequenceOwnedByColumn => { let sequence_option = name_ref .syntax() @@ -580,6 +587,10 @@ fn resolve_extension_name_ptr(binder: &Binder, extension_name: &Name) -> Option< binder.lookup(extension_name, SymbolKind::Extension) } +fn resolve_role_name_ptr(binder: &Binder, role_name: &Name) -> Option { + binder.lookup(role_name, SymbolKind::Role) +} + fn resolve_for_kind_with_params( binder: &Binder, name: &Name, diff --git a/crates/squawk_ide/src/symbols.rs b/crates/squawk_ide/src/symbols.rs index 5a3482f9..7d8b3b3f 100644 --- a/crates/squawk_ide/src/symbols.rs +++ b/crates/squawk_ide/src/symbols.rs @@ -62,6 +62,7 @@ pub(crate) enum SymbolKind { Extension, Trigger, EventTrigger, + Role, } #[derive(Clone, Debug)] diff --git a/crates/squawk_server/src/lib.rs b/crates/squawk_server/src/lib.rs index 4efde1c3..38b88708 100644 --- a/crates/squawk_server/src/lib.rs +++ b/crates/squawk_server/src/lib.rs @@ -381,6 +381,7 @@ fn handle_document_symbol( DocumentSymbolKind::PreparedStatement => SymbolKind::VARIABLE, DocumentSymbolKind::Channel => SymbolKind::EVENT, DocumentSymbolKind::EventTrigger => SymbolKind::EVENT, + DocumentSymbolKind::Role => SymbolKind::CLASS, }, tags: None, range, diff --git a/crates/squawk_wasm/src/lib.rs b/crates/squawk_wasm/src/lib.rs index 2b4466b7..83df6da1 100644 --- a/crates/squawk_wasm/src/lib.rs +++ b/crates/squawk_wasm/src/lib.rs @@ -407,6 +407,7 @@ fn convert_document_symbol( squawk_ide::document_symbols::DocumentSymbolKind::Aggregate => "aggregate", squawk_ide::document_symbols::DocumentSymbolKind::Procedure => "procedure", squawk_ide::document_symbols::DocumentSymbolKind::EventTrigger => "event_trigger", + squawk_ide::document_symbols::DocumentSymbolKind::Role => "role", squawk_ide::document_symbols::DocumentSymbolKind::Type => "type", squawk_ide::document_symbols::DocumentSymbolKind::Enum => "enum", squawk_ide::document_symbols::DocumentSymbolKind::Column => "column",