diff --git a/crates/pgt_hover/src/hovered_node.rs b/crates/pgt_hover/src/hovered_node.rs index 2a637bf7d..0d51a55d0 100644 --- a/crates/pgt_hover/src/hovered_node.rs +++ b/crates/pgt_hover/src/hovered_node.rs @@ -22,9 +22,9 @@ impl HoveredNode { pub(crate) fn get(ctx: &pgt_treesitter::context::TreesitterContext) -> Option { let node_content = ctx.get_node_under_cursor_content()?; - let under_node = ctx.node_under_cursor.as_ref()?; + let under_cursor = ctx.node_under_cursor.as_ref()?; - match under_node.kind() { + match under_cursor.kind() { "identifier" if ctx.matches_ancestor_history(&["relation", "object_reference"]) => { if let Some(schema) = ctx.schema_or_alias_name.as_ref() { Some(HoveredNode::Table(NodeIdentification::SchemaAndName(( @@ -64,6 +64,11 @@ impl HoveredNode { Some(HoveredNode::Role(NodeIdentification::Name(node_content))) } + // quoted columns + "literal" if ctx.matches_ancestor_history(&["select_expression", "term"]) => { + Some(HoveredNode::Column(NodeIdentification::Name(node_content))) + } + "policy_table" | "revoke_table" | "grant_table" => { if let Some(schema) = ctx.schema_or_alias_name.as_ref() { Some(HoveredNode::Table(NodeIdentification::SchemaAndName(( diff --git a/crates/pgt_hover/tests/hover_integration_tests.rs b/crates/pgt_hover/tests/hover_integration_tests.rs index c13f0c900..9882e6b6b 100644 --- a/crates/pgt_hover/tests/hover_integration_tests.rs +++ b/crates/pgt_hover/tests/hover_integration_tests.rs @@ -300,6 +300,149 @@ async fn test_role_hover_alter_role(test_db: PgPool) { } #[sqlx::test(migrator = "pgt_test_utils::MIGRATIONS")] +async fn test_table_hover_with_quoted_schema(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 "auth".use{}rs"#, + QueryWithCursorPosition::cursor_marker() + ); + + test_hover_at_cursor("table_hover_quoted_schema", query, Some(setup), &test_db).await; +} + +#[sqlx::test(migrator = "pgt_test_utils::MIGRATIONS")] +async fn test_function_hover_with_quoted_schema(test_db: PgPool) { + let setup = r#" + create schema auth; + create or replace function auth.authenticate_user(user_email text) + returns boolean + language plpgsql + security definer + as $$ + begin + return exists(select 1 from auth.users where email = user_email); + end; + $$; + "#; + + let query = format!( + r#"select "auth".authenticate_u{}ser('test@example.com')"#, + QueryWithCursorPosition::cursor_marker() + ); + + test_hover_at_cursor("function_hover_quoted_schema", query, Some(setup), &test_db).await; +} + +#[sqlx::test(migrator = "pgt_test_utils::MIGRATIONS")] +async fn test_column_hover_with_quoted_schema_table(test_db: PgPool) { + let setup = r#" + create schema auth; + create table auth.user_profiles ( + id serial primary key, + user_id int not null, + first_name varchar(100), + last_name varchar(100) + ); + "#; + + let query = format!( + r#"select "auth"."user_profiles".first_n{}ame from "auth"."user_profiles""#, + QueryWithCursorPosition::cursor_marker() + ); + + test_hover_at_cursor( + "column_hover_quoted_schema_table", + query, + Some(setup), + &test_db, + ) + .await; +} + +#[sqlx::test(migrator = "pgt_test_utils::MIGRATIONS")] +async fn test_table_hover_with_quoted_table_name(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 "auth"."use{}rs""#, + QueryWithCursorPosition::cursor_marker() + ); + + test_hover_at_cursor( + "table_hover_quoted_table_name", + query, + Some(setup), + &test_db, + ) + .await; +} + +#[sqlx::test(migrator = "pgt_test_utils::MIGRATIONS")] +async fn test_column_hover_with_quoted_column_name(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 "ema{}il" from auth.users"#, + QueryWithCursorPosition::cursor_marker() + ); + + test_hover_at_cursor( + "column_hover_quoted_column_name", + query, + Some(setup), + &test_db, + ) + .await; +} + +#[sqlx::test(migrator = "pgt_test_utils::MIGRATIONS")] +async fn test_column_hover_with_quoted_column_name_with_table(test_db: PgPool) { + let setup = r#" + create table users ( + id serial primary key, + email varchar(255) not null + ); + + create table phone_nums ( + phone_id serial primary key, + email varchar(255) not null, + phone int + ); + "#; + + let query = format!( + r#"select phone, id from users join phone_nums on "users"."em{}ail" = phone_nums.email;"#, + QueryWithCursorPosition::cursor_marker() + ); + + test_hover_at_cursor( + "column_hover_quoted_column_name_with_table", + query, + Some(setup), + &test_db, + ) + .await; +} + async fn test_policy_table_hover(test_db: PgPool) { let setup = r#" create table users ( diff --git a/crates/pgt_hover/tests/snapshots/column_hover_quoted_column_name.snap b/crates/pgt_hover/tests/snapshots/column_hover_quoted_column_name.snap new file mode 100644 index 000000000..e6f2654ed --- /dev/null +++ b/crates/pgt_hover/tests/snapshots/column_hover_quoted_column_name.snap @@ -0,0 +1,20 @@ +--- +source: crates/pgt_hover/tests/hover_integration_tests.rs +expression: snapshot +--- +# Input +```sql +select "email" from auth.users + ↑ hovered here +``` + +# Hover Results +### `auth.users.email` +```plain +varchar(255) - not null + +``` +--- +```plain + +``` diff --git a/crates/pgt_hover/tests/snapshots/column_hover_quoted_column_name_with_table.snap b/crates/pgt_hover/tests/snapshots/column_hover_quoted_column_name_with_table.snap new file mode 100644 index 000000000..76077f6ca --- /dev/null +++ b/crates/pgt_hover/tests/snapshots/column_hover_quoted_column_name_with_table.snap @@ -0,0 +1,20 @@ +--- +source: crates/pgt_hover/tests/hover_integration_tests.rs +expression: snapshot +--- +# Input +```sql +select phone, id from users join phone_nums on "users"."email" = phone_nums.email; + ↑ hovered here +``` + +# Hover Results +### `public.users.email` +```plain +varchar(255) - not null + +``` +--- +```plain + +``` diff --git a/crates/pgt_hover/tests/snapshots/column_hover_quoted_schema_table.snap b/crates/pgt_hover/tests/snapshots/column_hover_quoted_schema_table.snap new file mode 100644 index 000000000..aadb18458 --- /dev/null +++ b/crates/pgt_hover/tests/snapshots/column_hover_quoted_schema_table.snap @@ -0,0 +1,20 @@ +--- +source: crates/pgt_hover/tests/hover_integration_tests.rs +expression: snapshot +--- +# Input +```sql +select "auth"."user_profiles".first_name from "auth"."user_profiles" + ↑ hovered here +``` + +# Hover Results +### `auth.user_profiles.first_name` +```plain +varchar(100) - nullable + +``` +--- +```plain + +``` diff --git a/crates/pgt_hover/tests/snapshots/function_hover_quoted_schema.snap b/crates/pgt_hover/tests/snapshots/function_hover_quoted_schema.snap new file mode 100644 index 000000000..19653b223 --- /dev/null +++ b/crates/pgt_hover/tests/snapshots/function_hover_quoted_schema.snap @@ -0,0 +1,30 @@ +--- +source: crates/pgt_hover/tests/hover_integration_tests.rs +expression: snapshot +--- +# Input +```sql +select "auth".authenticate_user('test@example.com') + ↑ hovered here +``` + +# Hover Results +### `auth.authenticate_user(user_email text) → boolean` +```plain +Function - Security DEFINER +``` +--- +```sql + +CREATE OR REPLACE FUNCTION auth.authenticate_user(user_email text) + RETURNS boolean + LANGUAGE plpgsql + SECURITY DEFINER +AS $function$ + begin + return exists(select 1 from auth.users where email = user_email); + end; + $function$ + + +``` diff --git a/crates/pgt_hover/tests/snapshots/table_hover_quoted_schema.snap b/crates/pgt_hover/tests/snapshots/table_hover_quoted_schema.snap new file mode 100644 index 000000000..c62feed64 --- /dev/null +++ b/crates/pgt_hover/tests/snapshots/table_hover_quoted_schema.snap @@ -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.users` - 🔓 RLS disabled +```plain + +``` +--- +```plain + +~0 rows, ~0 dead rows, 8.19 kB +``` diff --git a/crates/pgt_hover/tests/snapshots/table_hover_quoted_table_name.snap b/crates/pgt_hover/tests/snapshots/table_hover_quoted_table_name.snap new file mode 100644 index 000000000..28147b3b0 --- /dev/null +++ b/crates/pgt_hover/tests/snapshots/table_hover_quoted_table_name.snap @@ -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.users` - 🔓 RLS disabled +```plain + +``` +--- +```plain + +~0 rows, ~0 dead rows, 8.19 kB +``` diff --git a/crates/pgt_schema_cache/src/schema_cache.rs b/crates/pgt_schema_cache/src/schema_cache.rs index 2c791a532..b2874671e 100644 --- a/crates/pgt_schema_cache/src/schema_cache.rs +++ b/crates/pgt_schema_cache/src/schema_cache.rs @@ -64,45 +64,86 @@ impl SchemaCache { } pub fn find_tables(&self, name: &str, schema: Option<&str>) -> Vec<&Table> { + let sanitized_name = Self::sanitize_identifier(name); self.tables .iter() - .filter(|t| t.name == name && schema.is_none_or(|s| s == t.schema.as_str())) + .filter(|t| { + t.name == sanitized_name + && schema + .map(Self::sanitize_identifier) + .as_deref() + .is_none_or(|s| s == t.schema.as_str()) + }) .collect() } pub fn find_type(&self, name: &str, schema: Option<&str>) -> Option<&PostgresType> { - self.types - .iter() - .find(|t| t.name == name && schema.is_none_or(|s| s == t.schema.as_str())) + let sanitized_name = Self::sanitize_identifier(name); + self.types.iter().find(|t| { + t.name == sanitized_name + && schema + .map(Self::sanitize_identifier) + .as_deref() + .is_none_or(|s| s == t.schema.as_str()) + }) } pub fn find_cols(&self, name: &str, table: Option<&str>, schema: Option<&str>) -> Vec<&Column> { + let sanitized_name = Self::sanitize_identifier(name); self.columns .iter() .filter(|c| { - c.name.as_str() == name - && table.is_none_or(|t| t == c.table_name.as_str()) - && schema.is_none_or(|s| s == c.schema_name.as_str()) + c.name.as_str() == sanitized_name + && table + .map(Self::sanitize_identifier) + .as_deref() + .is_none_or(|t| t == c.table_name.as_str()) + && schema + .map(Self::sanitize_identifier) + .as_deref() + .is_none_or(|s| s == c.schema_name.as_str()) }) .collect() } pub fn find_types(&self, name: &str, schema: Option<&str>) -> Vec<&PostgresType> { + let sanitized_name = Self::sanitize_identifier(name); self.types .iter() - .filter(|t| t.name == name && schema.is_none_or(|s| s == t.schema.as_str())) + .filter(|t| { + t.name == sanitized_name + && schema + .map(Self::sanitize_identifier) + .as_deref() + .is_none_or(|s| s == t.schema.as_str()) + }) .collect() } pub fn find_functions(&self, name: &str, schema: Option<&str>) -> Vec<&Function> { + let sanitized_name = Self::sanitize_identifier(name); self.functions .iter() - .filter(|f| f.name == name && schema.is_none_or(|s| s == f.schema.as_str())) + .filter(|f| { + f.name == sanitized_name + && schema + .map(Self::sanitize_identifier) + .as_deref() + .is_none_or(|s| s == f.schema.as_str()) + }) .collect() } pub fn find_roles(&self, name: &str) -> Vec<&Role> { - self.roles.iter().filter(|r| r.name == name).collect() + let sanitized_name = Self::sanitize_identifier(name); + self.roles + .iter() + .filter(|r| r.name == sanitized_name) + .collect() + } + + fn sanitize_identifier(identifier: &str) -> String { + identifier.replace('"', "") } }