Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions crates/pgt_hover/src/hoverables/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use crate::{contextual_priority::ContextualPriority, to_markdown::ToHoverMarkdow
mod column;
mod function;
mod role;
mod schema;
mod table;

mod test_helper;
Expand All @@ -14,6 +15,13 @@ pub enum Hoverable<'a> {
Column(&'a pgt_schema_cache::Column),
Function(&'a pgt_schema_cache::Function),
Role(&'a pgt_schema_cache::Role),
Schema(&'a pgt_schema_cache::Schema),
}

impl<'a> From<&'a pgt_schema_cache::Schema> for Hoverable<'a> {
fn from(value: &'a pgt_schema_cache::Schema) -> Self {
Hoverable::Schema(value)
}
}

impl<'a> From<&'a pgt_schema_cache::Table> for Hoverable<'a> {
Expand Down Expand Up @@ -47,6 +55,7 @@ impl ContextualPriority for Hoverable<'_> {
Hoverable::Column(column) => column.relevance_score(ctx),
Hoverable::Function(function) => function.relevance_score(ctx),
Hoverable::Role(role) => role.relevance_score(ctx),
Hoverable::Schema(schema) => schema.relevance_score(ctx),
}
}
}
Expand All @@ -58,6 +67,7 @@ impl ToHoverMarkdown for Hoverable<'_> {
Hoverable::Column(column) => ToHoverMarkdown::hover_headline(*column, writer),
Hoverable::Function(function) => ToHoverMarkdown::hover_headline(*function, writer),
Hoverable::Role(role) => ToHoverMarkdown::hover_headline(*role, writer),
Hoverable::Schema(schema) => ToHoverMarkdown::hover_headline(*schema, writer),
}
}

Expand All @@ -67,6 +77,7 @@ impl ToHoverMarkdown for Hoverable<'_> {
Hoverable::Column(column) => ToHoverMarkdown::hover_body(*column, writer),
Hoverable::Function(function) => ToHoverMarkdown::hover_body(*function, writer),
Hoverable::Role(role) => ToHoverMarkdown::hover_body(*role, writer),
Hoverable::Schema(schema) => ToHoverMarkdown::hover_body(*schema, writer),
}
}

Expand All @@ -76,6 +87,7 @@ impl ToHoverMarkdown for Hoverable<'_> {
Hoverable::Column(column) => ToHoverMarkdown::hover_footer(*column, writer),
Hoverable::Function(function) => ToHoverMarkdown::hover_footer(*function, writer),
Hoverable::Role(role) => ToHoverMarkdown::hover_footer(*role, writer),
Hoverable::Schema(schema) => ToHoverMarkdown::hover_footer(*schema, writer),
}
}

Expand All @@ -85,6 +97,7 @@ impl ToHoverMarkdown for Hoverable<'_> {
Hoverable::Column(column) => column.body_markdown_type(),
Hoverable::Function(function) => function.body_markdown_type(),
Hoverable::Role(role) => role.body_markdown_type(),
Hoverable::Schema(schema) => schema.body_markdown_type(),
}
}

Expand All @@ -94,6 +107,7 @@ impl ToHoverMarkdown for Hoverable<'_> {
Hoverable::Column(column) => column.footer_markdown_type(),
Hoverable::Function(function) => function.footer_markdown_type(),
Hoverable::Role(role) => role.footer_markdown_type(),
Hoverable::Schema(schema) => schema.footer_markdown_type(),
}
}
}
65 changes: 65 additions & 0 deletions crates/pgt_hover/src/hoverables/schema.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
use std::fmt::Write;

use pgt_schema_cache::Schema;
use pgt_treesitter::TreesitterContext;

use crate::{contextual_priority::ContextualPriority, to_markdown::ToHoverMarkdown};

impl ToHoverMarkdown for Schema {
fn hover_headline<W: Write>(&self, writer: &mut W) -> Result<(), std::fmt::Error> {
write!(writer, "`{}` - owned by {}", self.name, self.owner)?;

Ok(())
}

fn hover_body<W: Write>(&self, writer: &mut W) -> Result<bool, std::fmt::Error> {
if let Some(comment) = &self.comment {
write!(writer, "Comment: '{}'", comment)?;
writeln!(writer)?;
writeln!(writer)?;
}

if !self.allowed_creators.is_empty() {
write!(writer, "CREATE privileges:")?;
writeln!(writer)?;

for creator in &self.allowed_creators {
write!(writer, "- {}", creator)?;
writeln!(writer)?;
}

writeln!(writer)?;
}

if !self.allowed_users.is_empty() {
write!(writer, "USAGE privileges:")?;
writeln!(writer)?;

for user in &self.allowed_users {
write!(writer, "- {}", user)?;
writeln!(writer)?;
}

writeln!(writer)?;
}

Ok(true)
}

fn hover_footer<W: Write>(&self, writer: &mut W) -> Result<bool, std::fmt::Error> {
writeln!(writer)?;
write!(
writer,
"~{}, {} tables, {} views, {} functions",
self.total_size, self.table_count, self.view_count, self.function_count,
)?;
Ok(true)
}
}

impl ContextualPriority for Schema {
// there are no schemas with duplicate names.
fn relevance_score(&self, _ctx: &TreesitterContext) -> f32 {
0.0
}
}
5 changes: 5 additions & 0 deletions crates/pgt_hover/src/hovered_node.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@ impl HoveredNode {

match under_cursor.kind() {
"identifier" if ctx.matches_ancestor_history(&["relation", "object_reference"]) => {
let num_sibs = ctx.num_siblings();
if ctx.node_under_cursor_is_nth_child(1) && num_sibs > 0 {
return Some(HoveredNode::Schema(NodeIdentification::Name(node_content)));
}

if let Some(schema) = ctx.schema_or_alias_name.as_ref() {
Some(HoveredNode::Table(NodeIdentification::SchemaAndName((
schema.clone(),
Expand Down
11 changes: 11 additions & 0 deletions crates/pgt_hover/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,17 @@ pub fn on_hover(params: OnHoverParams) -> Vec<String> {
hovered_node::NodeIdentification::SchemaAndTableAndName(_) => vec![],
},

HoveredNode::Schema(node_identification) => match node_identification {
hovered_node::NodeIdentification::Name(schema_name) => params
.schema_cache
.find_schema(&schema_name)
.map(Hoverable::from)
.map(|s| vec![s])
.unwrap_or(vec![]),

_ => vec![],
},

_ => todo!(),
};

Expand Down
19 changes: 19 additions & 0 deletions crates/pgt_hover/tests/hover_integration_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -442,3 +442,22 @@ async fn test_column_hover_with_quoted_column_name_with_table(test_db: PgPool) {
)
.await;
}

#[sqlx::test(migrator = "pgt_test_utils::MIGRATIONS")]
async fn hover_on_schemas(test_db: PgPool) {
let setup = r#"
create schema auth;

create table auth.users (
id serial primary key,
email varchar(255) not null
);
"#;

let query = format!(
r#"select * from au{}th.users;"#,
QueryWithCursorPosition::cursor_marker()
);

test_hover_at_cursor("hover_on_schemas", query, Some(setup), &test_db).await;
}
20 changes: 20 additions & 0 deletions crates/pgt_hover/tests/snapshots/hover_on_schemas.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
---
source: crates/pgt_hover/tests/hover_integration_tests.rs
expression: snapshot
---
# Input
```sql
select * from auth.users;
↑ hovered here
```

# Hover Results
### `auth` - owned by postgres
```plain

```
---
```plain

~16 kB, 1 tables, 0 views, 0 functions
```
30 changes: 29 additions & 1 deletion crates/pgt_schema_cache/src/queries/schemas.sql
Original file line number Diff line number Diff line change
@@ -1,7 +1,35 @@
select
n.oid :: int8 as "id!",
n.nspname as name,
u.rolname as "owner!"
u.rolname as "owner!",
obj_description(n.oid, 'pg_namespace') as "comment",

coalesce((
select array_agg(grantee::regrole::text)
from aclexplode(n.nspacl)
where privilege_type = 'USAGE'
and grantee::regrole::text <> ''
and grantee::regrole::text <> '-'
Comment on lines +11 to +12
Copy link
Collaborator Author

@juleswritescode juleswritescode Sep 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i found some nameless roles in my local supabase database. Probably deleted?

select nspacl from pg_namespace where nspname = 'public' yielded:

{postgres=UC/postgres,anon=U/postgres,authenticated=U/postgres,service_role=U/postgres,=UC/postgres,airbyte=U/postgres,supabase_auth_admin=U/postgres}

), ARRAY[]::text[]) as "allowed_users!",

coalesce((
select array_agg(grantee::regrole::text)
from aclexplode(n.nspacl)
where privilege_type = 'CREATE'
and grantee::regrole::text <> ''
and grantee::regrole::text <> '-'
), ARRAY[]::text[]) as "allowed_creators!",

(select count(*) from pg_class c where c.relnamespace = n.oid and c.relkind = 'r') as "table_count!",
(select count(*) from pg_class c where c.relnamespace = n.oid and c.relkind = 'v') as "view_count!",
(select count(*) from pg_proc p where p.pronamespace = n.oid) as "function_count!",

coalesce(
(select pg_size_pretty(sum(pg_total_relation_size(c.oid)))
from pg_class c
where c.relnamespace = n.oid and c.relkind in ('r', 'i', 'm')),
'0 bytes'
) as "total_size!"
from
pg_namespace n,
pg_roles u
Expand Down
5 changes: 5 additions & 0 deletions crates/pgt_schema_cache/src/schema_cache.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,11 @@ impl SchemaCache {
})
}

pub fn find_schema(&self, name: &str) -> Option<&Schema> {
let sanitized_name = Self::sanitize_identifier(name);
self.schemas.iter().find(|s| s.name == sanitized_name)
}

pub fn find_tables(&self, name: &str, schema: Option<&str>) -> Vec<&Table> {
let sanitized_name = Self::sanitize_identifier(name);
self.tables
Expand Down
7 changes: 7 additions & 0 deletions crates/pgt_schema_cache/src/schemas.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,13 @@ pub struct Schema {
pub id: i64,
pub name: String,
pub owner: String,
pub allowed_users: Vec<String>,
pub allowed_creators: Vec<String>,
pub table_count: i64,
pub view_count: i64,
pub function_count: i64,
pub total_size: String,
pub comment: Option<String>,
}

impl SchemaCacheItem for Schema {
Expand Down
67 changes: 67 additions & 0 deletions crates/pgt_treesitter/src/context/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,15 @@ impl NodeUnderCursor<'_> {
NodeUnderCursor::CustomNode { kind, .. } => kind.as_str(),
}
}

pub fn has_prev_sibling(&self) -> bool {
match self {
NodeUnderCursor::TsNode(node) => node.prev_sibling().is_some(),
NodeUnderCursor::CustomNode {
previous_node_kind, ..
} => previous_node_kind.is_some(),
}
}
}

impl<'a> From<tree_sitter::Node<'a>> for NodeUnderCursor<'a> {
Expand Down Expand Up @@ -801,6 +810,64 @@ impl<'a> TreesitterContext<'a> {
})
}

/// Checks whether the Node under the cursor is the nth child of the parent.
///
/// ```
/// /*
/// * Given `select * from "a|uth"."users";`
/// * The node under the cursor is "auth".
/// *
/// * [...] redacted
/// * from [9..28] 'from "auth"."users"'
/// * keyword_from [9..13] 'from'
/// * relation [14..28] '"auth"."users"'
/// * object_reference [14..28] '"auth"."users"'
/// * identifier [14..20] '"auth"'
/// * . [20..21] '.'
/// * identifier [21..28] '"users"'
/// */
///
/// if node_under_cursor_is_nth_child(1) {
/// node_type = "schema";
/// } else if node_under_cursor_is_nth_child(3) {
/// node_type = "table";
/// }
/// ```
pub fn node_under_cursor_is_nth_child(&self, nth: usize) -> bool {
self.node_under_cursor
.as_ref()
.is_some_and(|under_cursor| match under_cursor {
NodeUnderCursor::TsNode(node) => {
let mut cursor = node.walk();
node.parent().is_some_and(|p| {
for (i, child) in p.children(&mut cursor).enumerate() {
if i + 1 == nth && child.id() == node.id() {
return true;
}
}

false
})
}
NodeUnderCursor::CustomNode { .. } => false,
})
}

/// Returns the number of siblings of the node under the cursor.
pub fn num_siblings(&self) -> usize {
self.node_under_cursor
.as_ref()
.map(|n| match n {
NodeUnderCursor::TsNode(node) => {
// if there's no parent, we're on the top of the tree,
// where we have 0 siblings.
node.parent().map(|p| p.child_count() - 1).unwrap_or(0)
}
NodeUnderCursor::CustomNode { .. } => 0,
})
.unwrap_or(0)
}

pub fn get_mentioned_relations(&self, key: &Option<String>) -> Option<&HashSet<String>> {
if let Some(key) = key.as_ref() {
let sanitized_key = key.replace('"', "");
Expand Down
Loading