From 6cba26450b5bff5d7a81a8235a9116ef130f6edc Mon Sep 17 00:00:00 2001 From: Julian Date: Thu, 11 Sep 2025 17:40:52 +0200 Subject: [PATCH 01/11] oK --- .../pgt_completions/src/providers/tables.rs | 53 +++++++++++++++++++ .../src/relevance/filtering.rs | 8 +-- 2 files changed, 57 insertions(+), 4 deletions(-) diff --git a/crates/pgt_completions/src/providers/tables.rs b/crates/pgt_completions/src/providers/tables.rs index 20100e01f..7759222bd 100644 --- a/crates/pgt_completions/src/providers/tables.rs +++ b/crates/pgt_completions/src/providers/tables.rs @@ -569,4 +569,57 @@ mod tests { ) .await; } + + #[sqlx::test(migrator = "pgt_test_utils::MIGRATIONS")] + async fn after_quoted_schemas(pool: PgPool) { + let setup = r#" + create schema auth; + + create table auth.users ( + uid serial primary key, + name text not null, + email text unique not null + ); + + create table auth.posts ( + pid serial primary key, + user_id int not null references auth.users(uid), + title text not null, + content text, + created_at timestamp default now() + ); + "#; + + pool.execute(setup).await.unwrap(); + + assert_complete_results( + format!( + r#"select * from "auth".{}"#, + QueryWithCursorPosition::cursor_marker() + ) + .as_str(), + vec![ + CompletionAssertion::LabelAndKind("posts".into(), CompletionItemKind::Table), + CompletionAssertion::LabelAndKind("users".into(), CompletionItemKind::Table), + ], + None, + &pool, + ) + .await; + + assert_complete_results( + format!( + r#"select * from "auth".{}"#, + QueryWithCursorPosition::cursor_marker() + ) + .as_str(), + vec![ + CompletionAssertion::LabelAndKind("posts".into(), CompletionItemKind::Table), + CompletionAssertion::LabelAndKind("users".into(), CompletionItemKind::Table), + ], + None, + &pool, + ) + .await; + } } diff --git a/crates/pgt_completions/src/relevance/filtering.rs b/crates/pgt_completions/src/relevance/filtering.rs index b9a896c49..e386bc5ea 100644 --- a/crates/pgt_completions/src/relevance/filtering.rs +++ b/crates/pgt_completions/src/relevance/filtering.rs @@ -249,13 +249,13 @@ impl CompletionFilter<'_> { return Some(()); } - let schema_or_alias = ctx.schema_or_alias_name.as_ref().unwrap(); + let schema_or_alias = ctx.schema_or_alias_name.as_ref().unwrap().replace('"', ""); let matches = match self.data { - CompletionRelevanceData::Table(table) => &table.schema == schema_or_alias, - CompletionRelevanceData::Function(f) => &f.schema == schema_or_alias, + CompletionRelevanceData::Table(table) => table.schema == schema_or_alias, + CompletionRelevanceData::Function(f) => f.schema == schema_or_alias, CompletionRelevanceData::Column(col) => ctx - .get_mentioned_table_for_alias(schema_or_alias) + .get_mentioned_table_for_alias(&schema_or_alias) .is_some_and(|t| t == &col.table_name), // we should never allow schema suggestions if there already was one. From 77d2649fe5024bf8a47a54b8d1eda8be282a06af Mon Sep 17 00:00:00 2001 From: Julian Date: Thu, 11 Sep 2025 18:18:30 +0200 Subject: [PATCH 02/11] ack --- .../src/providers/functions.rs | 75 ++++++++++++++++++- .../pgt_completions/src/providers/helper.rs | 15 ++-- .../pgt_completions/src/providers/tables.rs | 44 +++++++++-- crates/pgt_completions/src/sanitization.rs | 56 +++++++++++--- 4 files changed, 167 insertions(+), 23 deletions(-) diff --git a/crates/pgt_completions/src/providers/functions.rs b/crates/pgt_completions/src/providers/functions.rs index f4e86509b..876b34a47 100644 --- a/crates/pgt_completions/src/providers/functions.rs +++ b/crates/pgt_completions/src/providers/functions.rs @@ -1,10 +1,11 @@ use pgt_schema_cache::{Function, SchemaCache}; +use pgt_text_size::TextSize; use pgt_treesitter::TreesitterContext; use crate::{ CompletionItemKind, CompletionText, builder::{CompletionBuilder, PossibleCompletionItem}, - providers::helper::get_range_to_replace, + providers::helper::{get_range_to_replace, node_text_surrounded_by_quotes, only_leading_quote}, relevance::{CompletionRelevanceData, filtering::CompletionFilter, scoring::CompletionScore}, }; @@ -37,7 +38,7 @@ pub fn complete_functions<'a>( fn get_completion_text(ctx: &TreesitterContext, func: &Function) -> CompletionText { let mut text = with_schema_or_alias(ctx, func.name.as_str(), Some(func.schema.as_str())); - let range = get_range_to_replace(ctx); + let mut range = get_range_to_replace(ctx); if ctx.is_invocation { CompletionText { @@ -46,6 +47,11 @@ fn get_completion_text(ctx: &TreesitterContext, func: &Function) -> CompletionTe is_snippet: false, } } else { + if node_text_surrounded_by_quotes(ctx) && !only_leading_quote(ctx) { + text.push('"'); + range = range.checked_expand_end(1.into()).unwrap_or(range); + } + text.push('('); let num_args = func.args.args.len(); @@ -68,6 +74,7 @@ fn get_completion_text(ctx: &TreesitterContext, func: &Function) -> CompletionTe #[cfg(test)] mod tests { + use pgt_text_size::TextRange; use sqlx::{Executor, PgPool}; use crate::{ @@ -294,4 +301,68 @@ mod tests { ) .await; } + + #[sqlx::test(migrator = "pgt_test_utils::MIGRATIONS")] + async fn autocompletes_after_schema_in_quotes(pool: PgPool) { + let setup = r#" + create schema auth; + + create or replace function auth.my_cool_foo() + returns trigger + language plpgsql + security invoker + as $$ + begin + raise exception 'dont matter'; + end; + $$; + "#; + + pool.execute(setup).await.unwrap(); + + assert_complete_results( + format!( + r#"select "auth".{}"#, + QueryWithCursorPosition::cursor_marker() + ) + .as_str(), + vec![CompletionAssertion::CompletionTextAndRange( + "my_cool_foo()".into(), + TextRange::new(14.into(), 14.into()), + )], + None, + &pool, + ) + .await; + + assert_complete_results( + format!( + r#"select "auth"."{}"#, + QueryWithCursorPosition::cursor_marker() + ) + .as_str(), + vec![CompletionAssertion::CompletionTextAndRange( + r#"my_cool_foo"()"#.into(), + TextRange::new(15.into(), 15.into()), + )], + None, + &pool, + ) + .await; + + assert_complete_results( + format!( + r#"select "auth"."{}""#, + QueryWithCursorPosition::cursor_marker() + ) + .as_str(), + vec![CompletionAssertion::CompletionTextAndRange( + r#"my_cool_foo"()"#.into(), + TextRange::new(15.into(), 16.into()), + )], + None, + &pool, + ) + .await; + } } diff --git a/crates/pgt_completions/src/providers/helper.rs b/crates/pgt_completions/src/providers/helper.rs index b6547d701..002e8649e 100644 --- a/crates/pgt_completions/src/providers/helper.rs +++ b/crates/pgt_completions/src/providers/helper.rs @@ -34,6 +34,12 @@ pub(crate) fn get_range_to_replace(ctx: &TreesitterContext) -> TextRange { } } +pub(crate) fn only_leading_quote(ctx: &TreesitterContext) -> bool { + let node_under_cursor_txt = ctx.get_node_under_cursor_content().unwrap_or("".into()); + let node_under_cursor_txt = node_under_cursor_txt.as_str(); + is_sanitized_token_with_quote(node_under_cursor_txt) +} + pub(crate) fn with_schema_or_alias( ctx: &TreesitterContext, item_name: &str, @@ -42,13 +48,10 @@ pub(crate) fn with_schema_or_alias( let is_already_prefixed_with_schema_name = ctx.schema_or_alias_name.is_some(); let with_quotes = node_text_surrounded_by_quotes(ctx); - - let node_under_cursor_txt = ctx.get_node_under_cursor_content().unwrap_or("".into()); - let node_under_cursor_txt = node_under_cursor_txt.as_str(); - let is_quote_sanitized = is_sanitized_token_with_quote(node_under_cursor_txt); + let single_leading_quote = only_leading_quote(ctx); if schema_or_alias_name.is_none_or(|s| s == "public") || is_already_prefixed_with_schema_name { - if is_quote_sanitized { + if single_leading_quote { format!(r#"{}""#, item_name) } else { item_name.to_string() @@ -56,7 +59,7 @@ pub(crate) fn with_schema_or_alias( } else { let schema_or_als = schema_or_alias_name.unwrap(); - if is_quote_sanitized { + if single_leading_quote { format!(r#"{}"."{}""#, schema_or_als.replace('"', ""), item_name) } else if with_quotes { format!(r#"{}"."{}"#, schema_or_als.replace('"', ""), item_name) diff --git a/crates/pgt_completions/src/providers/tables.rs b/crates/pgt_completions/src/providers/tables.rs index 7759222bd..6ef9a6b90 100644 --- a/crates/pgt_completions/src/providers/tables.rs +++ b/crates/pgt_completions/src/providers/tables.rs @@ -58,6 +58,7 @@ fn get_completion_text(ctx: &TreesitterContext, table: &Table) -> CompletionText #[cfg(test)] mod tests { + use pgt_text_size::TextRange; use sqlx::{Executor, PgPool}; use crate::{ @@ -599,8 +600,14 @@ mod tests { ) .as_str(), vec![ - CompletionAssertion::LabelAndKind("posts".into(), CompletionItemKind::Table), - CompletionAssertion::LabelAndKind("users".into(), CompletionItemKind::Table), + CompletionAssertion::CompletionTextAndRange( + "posts".into(), + TextRange::new(21.into(), 21.into()), + ), + CompletionAssertion::CompletionTextAndRange( + "users".into(), + TextRange::new(21.into(), 21.into()), + ), ], None, &pool, @@ -609,13 +616,40 @@ mod tests { assert_complete_results( format!( - r#"select * from "auth".{}"#, + r#"select * from "auth"."{}""#, QueryWithCursorPosition::cursor_marker() ) .as_str(), vec![ - CompletionAssertion::LabelAndKind("posts".into(), CompletionItemKind::Table), - CompletionAssertion::LabelAndKind("users".into(), CompletionItemKind::Table), + CompletionAssertion::CompletionTextAndRange( + "posts".into(), + TextRange::new(22.into(), 22.into()), + ), + CompletionAssertion::CompletionTextAndRange( + "users".into(), + TextRange::new(22.into(), 22.into()), + ), + ], + None, + &pool, + ) + .await; + + assert_complete_results( + format!( + r#"select * from "auth"."{}"#, + QueryWithCursorPosition::cursor_marker() + ) + .as_str(), + vec![ + CompletionAssertion::CompletionTextAndRange( + r#"posts""#.into(), + TextRange::new(22.into(), 22.into()), + ), + CompletionAssertion::CompletionTextAndRange( + r#"users""#.into(), + TextRange::new(22.into(), 22.into()), + ), ], None, &pool, diff --git a/crates/pgt_completions/src/sanitization.rs b/crates/pgt_completions/src/sanitization.rs index 5272c75e2..1d728ffca 100644 --- a/crates/pgt_completions/src/sanitization.rs +++ b/crates/pgt_completions/src/sanitization.rs @@ -98,7 +98,11 @@ where // we want to push spaces until we arrive at the cursor position. // we'll then add the SANITIZED_TOKEN if idx == cursor_pos { - sql.push_str(SANITIZED_TOKEN); + if opened_quote && has_uneven_quotes { + sql.push_str(SANITIZED_TOKEN_WITH_QUOTE); + } else { + sql.push_str(SANITIZED_TOKEN); + } } else { sql.push(' '); } @@ -342,18 +346,50 @@ mod tests { #[test] fn should_sanitize_with_opened_quotes() { - // select "email", "| from "auth"."users"; - let input = r#"select "email", " from "auth"."users";"#; - let position = TextSize::new(17); + { + // select "email", "| from "auth"."users"; + let input = r#"select "email", " from "auth"."users";"#; + let position = TextSize::new(17); - let params = get_test_params(input, position); + let params = get_test_params(input, position); - let sanitized = SanitizedCompletionParams::from(params); + let sanitized = SanitizedCompletionParams::from(params); - assert_eq!( - sanitized.text, - r#"select "email", "REPLACED_TOKEN_WITH_QUOTE" from "auth"."users";"# - ); + assert_eq!( + sanitized.text, + r#"select "email", "REPLACED_TOKEN_WITH_QUOTE" from "auth"."users";"# + ); + } + + { + // select * from "auth"."|; <-- with semi + let input = r#"select * from "auth".";"#; + let position = TextSize::new(22); + + let params = get_test_params(input, position); + + let sanitized = SanitizedCompletionParams::from(params); + + assert_eq!( + sanitized.text, + r#"select * from "auth"."REPLACED_TOKEN_WITH_QUOTE";"# + ); + } + + { + // select * from "auth"."| <-- without semi + let input = r#"select * from "auth".""#; + let position = TextSize::new(22); + + let params = get_test_params(input, position); + + let sanitized = SanitizedCompletionParams::from(params); + + assert_eq!( + sanitized.text, + r#"select * from "auth"."REPLACED_TOKEN_WITH_QUOTE""# + ); + } } #[test] From 79cdcc402d3033a8f26f4760144f50996f459eb8 Mon Sep 17 00:00:00 2001 From: Julian Date: Thu, 11 Sep 2025 18:28:39 +0200 Subject: [PATCH 03/11] wowa wiwa --- crates/pgt_completions/src/providers/functions.rs | 1 - crates/pgt_completions/src/relevance/scoring.rs | 3 ++- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/pgt_completions/src/providers/functions.rs b/crates/pgt_completions/src/providers/functions.rs index 876b34a47..7636d3595 100644 --- a/crates/pgt_completions/src/providers/functions.rs +++ b/crates/pgt_completions/src/providers/functions.rs @@ -1,5 +1,4 @@ use pgt_schema_cache::{Function, SchemaCache}; -use pgt_text_size::TextSize; use pgt_treesitter::TreesitterContext; use crate::{ diff --git a/crates/pgt_completions/src/relevance/scoring.rs b/crates/pgt_completions/src/relevance/scoring.rs index ba45e2d0d..e4f8eb5a2 100644 --- a/crates/pgt_completions/src/relevance/scoring.rs +++ b/crates/pgt_completions/src/relevance/scoring.rs @@ -184,9 +184,10 @@ impl CompletionScore<'_> { } fn check_matches_schema(&mut self, ctx: &TreesitterContext) { + // TODO let schema_name = match ctx.schema_or_alias_name.as_ref() { None => return, - Some(n) => n, + Some(n) => n.replace('"', ""), }; let data_schema = match self.get_schema_name() { From 67067dcb17d7f479c2218faaee7398704b345adf Mon Sep 17 00:00:00 2001 From: Julian Date: Fri, 12 Sep 2025 07:42:20 +0200 Subject: [PATCH 04/11] lgtm --- crates/pgt_hover/src/hovered_node.rs | 9 +- .../tests/hover_integration_tests.rs | 144 ++++++++++++++++++ .../column_hover_quoted_column_name.snap | 20 +++ ...n_hover_quoted_column_name_with_table.snap | 20 +++ .../column_hover_quoted_schema_table.snap | 20 +++ .../function_hover_quoted_schema.snap | 30 ++++ .../snapshots/table_hover_quoted_schema.snap | 20 +++ .../table_hover_quoted_table_name.snap | 20 +++ crates/pgt_schema_cache/src/schema_cache.rs | 62 ++++++-- 9 files changed, 333 insertions(+), 12 deletions(-) create mode 100644 crates/pgt_hover/tests/snapshots/column_hover_quoted_column_name.snap create mode 100644 crates/pgt_hover/tests/snapshots/column_hover_quoted_column_name_with_table.snap create mode 100644 crates/pgt_hover/tests/snapshots/column_hover_quoted_schema_table.snap create mode 100644 crates/pgt_hover/tests/snapshots/function_hover_quoted_schema.snap create mode 100644 crates/pgt_hover/tests/snapshots/table_hover_quoted_schema.snap create mode 100644 crates/pgt_hover/tests/snapshots/table_hover_quoted_table_name.snap diff --git a/crates/pgt_hover/src/hovered_node.rs b/crates/pgt_hover/src/hovered_node.rs index 44c8041cb..3dc35daee 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))) + } + _ => None, } } diff --git a/crates/pgt_hover/tests/hover_integration_tests.rs b/crates/pgt_hover/tests/hover_integration_tests.rs index d63daea2e..3f2434cc7 100644 --- a/crates/pgt_hover/tests/hover_integration_tests.rs +++ b/crates/pgt_hover/tests/hover_integration_tests.rs @@ -298,3 +298,147 @@ async fn test_role_hover_alter_role(test_db: PgPool) { test_hover_at_cursor("role_alter", query, None, &test_db).await; } + +#[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; +} 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..5ada7ea04 100644 --- a/crates/pgt_schema_cache/src/schema_cache.rs +++ b/crates/pgt_schema_cache/src/schema_cache.rs @@ -64,45 +64,87 @@ 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); + println!("sanitized name: {:#?}", sanitized_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('"', "") } } From 7484ccd5c65a14269a0b060264acf8ae74eed278 Mon Sep 17 00:00:00 2001 From: Julian Domke <68325451+juleswritescode@users.noreply.github.com> Date: Fri, 12 Sep 2025 07:43:49 +0200 Subject: [PATCH 05/11] Update crates/pgt_schema_cache/src/schema_cache.rs --- crates/pgt_schema_cache/src/schema_cache.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/crates/pgt_schema_cache/src/schema_cache.rs b/crates/pgt_schema_cache/src/schema_cache.rs index 5ada7ea04..b2874671e 100644 --- a/crates/pgt_schema_cache/src/schema_cache.rs +++ b/crates/pgt_schema_cache/src/schema_cache.rs @@ -90,7 +90,6 @@ impl SchemaCache { pub fn find_cols(&self, name: &str, table: Option<&str>, schema: Option<&str>) -> Vec<&Column> { let sanitized_name = Self::sanitize_identifier(name); - println!("sanitized name: {:#?}", sanitized_name); self.columns .iter() .filter(|c| { From 7037ce920c44e89248522d7e84bf82dcb0d58e8c Mon Sep 17 00:00:00 2001 From: Julian Date: Sat, 13 Sep 2025 08:19:22 +0200 Subject: [PATCH 06/11] ok --- crates/pgt_hover/src/hoverables/mod.rs | 14 ++++ crates/pgt_hover/src/hoverables/schema.rs | 64 +++++++++++++++++++ crates/pgt_hover/src/hovered_node.rs | 4 ++ crates/pgt_hover/src/lib.rs | 16 ++++- .../tests/hover_integration_tests.rs | 19 ++++++ .../tests/snapshots/hover_on_schemas.snap.new | 21 ++++++ .../pgt_schema_cache/src/queries/schemas.sql | 26 +++++++- crates/pgt_schema_cache/src/schema_cache.rs | 5 ++ crates/pgt_schema_cache/src/schemas.rs | 7 ++ crates/pgt_treesitter/src/context/mod.rs | 52 +++++++++++++++ 10 files changed, 226 insertions(+), 2 deletions(-) create mode 100644 crates/pgt_hover/src/hoverables/schema.rs create mode 100644 crates/pgt_hover/tests/snapshots/hover_on_schemas.snap.new diff --git a/crates/pgt_hover/src/hoverables/mod.rs b/crates/pgt_hover/src/hoverables/mod.rs index 08feada8d..675c1366a 100644 --- a/crates/pgt_hover/src/hoverables/mod.rs +++ b/crates/pgt_hover/src/hoverables/mod.rs @@ -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; @@ -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> { @@ -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), } } } @@ -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), } } @@ -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), } } @@ -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), } } @@ -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(), } } @@ -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(), } } } diff --git a/crates/pgt_hover/src/hoverables/schema.rs b/crates/pgt_hover/src/hoverables/schema.rs new file mode 100644 index 000000000..9a8efc9f1 --- /dev/null +++ b/crates/pgt_hover/src/hoverables/schema.rs @@ -0,0 +1,64 @@ +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(&self, writer: &mut W) -> Result<(), std::fmt::Error> { + write!(writer, "`{}` - owned by {}", self.name, self.owner)?; + + Ok(()) + } + + fn hover_body(&self, writer: &mut W) -> Result { + if let Some(comment) = &self.comment { + write!(writer, "Comment: '{}'", comment)?; + 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(&self, writer: &mut W) -> Result { + 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 + } +} diff --git a/crates/pgt_hover/src/hovered_node.rs b/crates/pgt_hover/src/hovered_node.rs index 3dc35daee..0ada6ce79 100644 --- a/crates/pgt_hover/src/hovered_node.rs +++ b/crates/pgt_hover/src/hovered_node.rs @@ -26,6 +26,10 @@ impl HoveredNode { match under_cursor.kind() { "identifier" if ctx.matches_ancestor_history(&["relation", "object_reference"]) => { + if ctx.node_under_cursor_is_nth_child(1) && under_cursor.has_prev_sibling() { + 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(), diff --git a/crates/pgt_hover/src/lib.rs b/crates/pgt_hover/src/lib.rs index f3b8a2640..158bf8019 100644 --- a/crates/pgt_hover/src/lib.rs +++ b/crates/pgt_hover/src/lib.rs @@ -103,7 +103,21 @@ pub fn on_hover(params: OnHoverParams) -> Vec { hovered_node::NodeIdentification::SchemaAndTableAndName(_) => vec![], }, - _ => todo!(), + 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![], + }, + + t => { + println!("{:?}", t); + todo!() + } }; prioritize_by_context(items, &ctx) diff --git a/crates/pgt_hover/tests/hover_integration_tests.rs b/crates/pgt_hover/tests/hover_integration_tests.rs index 3f2434cc7..739b9e591 100644 --- a/crates/pgt_hover/tests/hover_integration_tests.rs +++ b/crates/pgt_hover/tests/hover_integration_tests.rs @@ -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; +} diff --git a/crates/pgt_hover/tests/snapshots/hover_on_schemas.snap.new b/crates/pgt_hover/tests/snapshots/hover_on_schemas.snap.new new file mode 100644 index 000000000..f190aee90 --- /dev/null +++ b/crates/pgt_hover/tests/snapshots/hover_on_schemas.snap.new @@ -0,0 +1,21 @@ +--- +source: crates/pgt_hover/tests/hover_integration_tests.rs +assertion_line: 69 +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 +``` diff --git a/crates/pgt_schema_cache/src/queries/schemas.sql b/crates/pgt_schema_cache/src/queries/schemas.sql index 55e1824b4..7c0106ed9 100644 --- a/crates/pgt_schema_cache/src/queries/schemas.sql +++ b/crates/pgt_schema_cache/src/queries/schemas.sql @@ -1,7 +1,31 @@ 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' + ), ARRAY[]::text[]) as "allowed_users!", + + coalesce(( + select array_agg(grantee::regrole::text) + from aclexplode(n.nspacl) + where privilege_type = 'CREATE' + ), 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 diff --git a/crates/pgt_schema_cache/src/schema_cache.rs b/crates/pgt_schema_cache/src/schema_cache.rs index b2874671e..6fbdfcdc3 100644 --- a/crates/pgt_schema_cache/src/schema_cache.rs +++ b/crates/pgt_schema_cache/src/schema_cache.rs @@ -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 diff --git a/crates/pgt_schema_cache/src/schemas.rs b/crates/pgt_schema_cache/src/schemas.rs index 5a007e511..21049ff11 100644 --- a/crates/pgt_schema_cache/src/schemas.rs +++ b/crates/pgt_schema_cache/src/schemas.rs @@ -7,6 +7,13 @@ pub struct Schema { pub id: i64, pub name: String, pub owner: String, + pub allowed_users: Vec, + pub allowed_creators: Vec, + pub table_count: i64, + pub view_count: i64, + pub function_count: i64, + pub total_size: String, + pub comment: Option, } impl SchemaCacheItem for Schema { diff --git a/crates/pgt_treesitter/src/context/mod.rs b/crates/pgt_treesitter/src/context/mod.rs index d481af8cb..bbdaad19e 100644 --- a/crates/pgt_treesitter/src/context/mod.rs +++ b/crates/pgt_treesitter/src/context/mod.rs @@ -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> for NodeUnderCursor<'a> { @@ -801,6 +810,49 @@ 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, + }) + } + pub fn get_mentioned_relations(&self, key: &Option) -> Option<&HashSet> { if let Some(key) = key.as_ref() { let sanitized_key = key.replace('"', ""); From 7a1f72e5b50f793c24b4a9b0c713334988d6a3d4 Mon Sep 17 00:00:00 2001 From: Julian Date: Sat, 13 Sep 2025 08:36:46 +0200 Subject: [PATCH 07/11] might be it --- crates/pgt_hover/src/hoverables/schema.rs | 1 + crates/pgt_hover/src/hovered_node.rs | 3 ++- ..._on_schemas.snap.new => hover_on_schemas.snap} | 1 - crates/pgt_schema_cache/src/queries/schemas.sql | 4 ++++ crates/pgt_treesitter/src/context/mod.rs | 15 +++++++++++++++ 5 files changed, 22 insertions(+), 2 deletions(-) rename crates/pgt_hover/tests/snapshots/{hover_on_schemas.snap.new => hover_on_schemas.snap} (94%) diff --git a/crates/pgt_hover/src/hoverables/schema.rs b/crates/pgt_hover/src/hoverables/schema.rs index 9a8efc9f1..cb45a3c9b 100644 --- a/crates/pgt_hover/src/hoverables/schema.rs +++ b/crates/pgt_hover/src/hoverables/schema.rs @@ -16,6 +16,7 @@ impl ToHoverMarkdown for Schema { if let Some(comment) = &self.comment { write!(writer, "Comment: '{}'", comment)?; writeln!(writer)?; + writeln!(writer)?; } if !self.allowed_creators.is_empty() { diff --git a/crates/pgt_hover/src/hovered_node.rs b/crates/pgt_hover/src/hovered_node.rs index 0ada6ce79..d4372b58a 100644 --- a/crates/pgt_hover/src/hovered_node.rs +++ b/crates/pgt_hover/src/hovered_node.rs @@ -26,7 +26,8 @@ impl HoveredNode { match under_cursor.kind() { "identifier" if ctx.matches_ancestor_history(&["relation", "object_reference"]) => { - if ctx.node_under_cursor_is_nth_child(1) && under_cursor.has_prev_sibling() { + 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))); } diff --git a/crates/pgt_hover/tests/snapshots/hover_on_schemas.snap.new b/crates/pgt_hover/tests/snapshots/hover_on_schemas.snap similarity index 94% rename from crates/pgt_hover/tests/snapshots/hover_on_schemas.snap.new rename to crates/pgt_hover/tests/snapshots/hover_on_schemas.snap index f190aee90..0897ea6cb 100644 --- a/crates/pgt_hover/tests/snapshots/hover_on_schemas.snap.new +++ b/crates/pgt_hover/tests/snapshots/hover_on_schemas.snap @@ -1,6 +1,5 @@ --- source: crates/pgt_hover/tests/hover_integration_tests.rs -assertion_line: 69 expression: snapshot --- # Input diff --git a/crates/pgt_schema_cache/src/queries/schemas.sql b/crates/pgt_schema_cache/src/queries/schemas.sql index 7c0106ed9..d22fb3c48 100644 --- a/crates/pgt_schema_cache/src/queries/schemas.sql +++ b/crates/pgt_schema_cache/src/queries/schemas.sql @@ -8,12 +8,16 @@ select select array_agg(grantee::regrole::text) from aclexplode(n.nspacl) where privilege_type = 'USAGE' + and grantee::regrole::text <> '' + and grantee::regrole::text <> '-' ), 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!", diff --git a/crates/pgt_treesitter/src/context/mod.rs b/crates/pgt_treesitter/src/context/mod.rs index bbdaad19e..ecd7c9973 100644 --- a/crates/pgt_treesitter/src/context/mod.rs +++ b/crates/pgt_treesitter/src/context/mod.rs @@ -853,6 +853,21 @@ impl<'a> TreesitterContext<'a> { }) } + /// 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) -> Option<&HashSet> { if let Some(key) = key.as_ref() { let sanitized_key = key.replace('"', ""); From df264f3f25d9e8fbee721c1546171147feef1eb5 Mon Sep 17 00:00:00 2001 From: Julian Domke <68325451+juleswritescode@users.noreply.github.com> Date: Sat, 13 Sep 2025 08:39:14 +0200 Subject: [PATCH 08/11] Update crates/pgt_hover/src/lib.rs --- crates/pgt_hover/src/lib.rs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/crates/pgt_hover/src/lib.rs b/crates/pgt_hover/src/lib.rs index 158bf8019..4878e138c 100644 --- a/crates/pgt_hover/src/lib.rs +++ b/crates/pgt_hover/src/lib.rs @@ -114,10 +114,7 @@ pub fn on_hover(params: OnHoverParams) -> Vec { _ => vec![], }, - t => { - println!("{:?}", t); - todo!() - } + _ => todo!(), }; prioritize_by_context(items, &ctx) From 897260664b05b0d2e3367037125d9343ec97e1b2 Mon Sep 17 00:00:00 2001 From: Julian Domke <68325451+juleswritescode@users.noreply.github.com> Date: Sat, 13 Sep 2025 08:41:18 +0200 Subject: [PATCH 09/11] Update crates/pgt_treesitter/src/context/mod.rs --- crates/pgt_treesitter/src/context/mod.rs | 9 --------- 1 file changed, 9 deletions(-) diff --git a/crates/pgt_treesitter/src/context/mod.rs b/crates/pgt_treesitter/src/context/mod.rs index ecd7c9973..104fe899d 100644 --- a/crates/pgt_treesitter/src/context/mod.rs +++ b/crates/pgt_treesitter/src/context/mod.rs @@ -104,15 +104,6 @@ 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> for NodeUnderCursor<'a> { From 114b279d5bbe8fadbd041a9201d8252779f48243 Mon Sep 17 00:00:00 2001 From: Julian Date: Sat, 13 Sep 2025 08:43:59 +0200 Subject: [PATCH 10/11] optimize --- crates/pgt_hover/src/lib.rs | 2 +- crates/pgt_treesitter/src/context/mod.rs | 10 +++------- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/crates/pgt_hover/src/lib.rs b/crates/pgt_hover/src/lib.rs index 4878e138c..c065a0611 100644 --- a/crates/pgt_hover/src/lib.rs +++ b/crates/pgt_hover/src/lib.rs @@ -109,7 +109,7 @@ pub fn on_hover(params: OnHoverParams) -> Vec { .find_schema(&schema_name) .map(Hoverable::from) .map(|s| vec![s]) - .unwrap_or(vec![]), + .unwrap_or_default(), _ => vec![], }, diff --git a/crates/pgt_treesitter/src/context/mod.rs b/crates/pgt_treesitter/src/context/mod.rs index 104fe899d..c201e1d0e 100644 --- a/crates/pgt_treesitter/src/context/mod.rs +++ b/crates/pgt_treesitter/src/context/mod.rs @@ -831,13 +831,9 @@ impl<'a> TreesitterContext<'a> { 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 + p.children(&mut cursor) + .nth(nth - 1) + .is_some_and(|n| n.id() == node.id()) }) } NodeUnderCursor::CustomNode { .. } => false, From f22ab9efa58bd3939f704c29295eb8c0543140e3 Mon Sep 17 00:00:00 2001 From: Julian Date: Sat, 13 Sep 2025 09:01:11 +0200 Subject: [PATCH 11/11] cargo sqlx prepare --workspace --- ...48e76ff7e255960a4ce5466674ff35a97b151.json | 32 -------- ...33a1121d69c7dca3c0e5e202835d9fefaa7cc.json | 74 +++++++++++++++++++ justfile | 1 + 3 files changed, 75 insertions(+), 32 deletions(-) delete mode 100644 .sqlx/query-36862f7f9d2d1c50ba253b28a7648e76ff7e255960a4ce5466674ff35a97b151.json create mode 100644 .sqlx/query-c775d3eaa6e95504de411dd5c2433a1121d69c7dca3c0e5e202835d9fefaa7cc.json diff --git a/.sqlx/query-36862f7f9d2d1c50ba253b28a7648e76ff7e255960a4ce5466674ff35a97b151.json b/.sqlx/query-36862f7f9d2d1c50ba253b28a7648e76ff7e255960a4ce5466674ff35a97b151.json deleted file mode 100644 index 6255c9b98..000000000 --- a/.sqlx/query-36862f7f9d2d1c50ba253b28a7648e76ff7e255960a4ce5466674ff35a97b151.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "select\n n.oid :: int8 as \"id!\",\n n.nspname as name,\n u.rolname as \"owner!\"\nfrom\n pg_namespace n,\n pg_roles u\nwhere\n n.nspowner = u.oid\n and (\n pg_has_role(n.nspowner, 'USAGE')\n or has_schema_privilege(n.oid, 'CREATE, USAGE')\n )\n and not pg_catalog.starts_with(n.nspname, 'pg_temp_')\n and not pg_catalog.starts_with(n.nspname, 'pg_toast_temp_');", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "id!", - "type_info": "Int8" - }, - { - "ordinal": 1, - "name": "name", - "type_info": "Name" - }, - { - "ordinal": 2, - "name": "owner!", - "type_info": "Name" - } - ], - "parameters": { - "Left": [] - }, - "nullable": [ - null, - false, - true - ] - }, - "hash": "36862f7f9d2d1c50ba253b28a7648e76ff7e255960a4ce5466674ff35a97b151" -} diff --git a/.sqlx/query-c775d3eaa6e95504de411dd5c2433a1121d69c7dca3c0e5e202835d9fefaa7cc.json b/.sqlx/query-c775d3eaa6e95504de411dd5c2433a1121d69c7dca3c0e5e202835d9fefaa7cc.json new file mode 100644 index 000000000..b91935fa1 --- /dev/null +++ b/.sqlx/query-c775d3eaa6e95504de411dd5c2433a1121d69c7dca3c0e5e202835d9fefaa7cc.json @@ -0,0 +1,74 @@ +{ + "db_name": "PostgreSQL", + "query": "select\n n.oid :: int8 as \"id!\",\n n.nspname as name,\n u.rolname as \"owner!\",\n obj_description(n.oid, 'pg_namespace') as \"comment\",\n\n coalesce((\n select array_agg(grantee::regrole::text)\n from aclexplode(n.nspacl)\n where privilege_type = 'USAGE'\n and grantee::regrole::text <> ''\n and grantee::regrole::text <> '-'\n ), ARRAY[]::text[]) as \"allowed_users!\",\n\n coalesce((\n select array_agg(grantee::regrole::text)\n from aclexplode(n.nspacl)\n where privilege_type = 'CREATE'\n and grantee::regrole::text <> ''\n and grantee::regrole::text <> '-'\n ), ARRAY[]::text[]) as \"allowed_creators!\",\n\n (select count(*) from pg_class c where c.relnamespace = n.oid and c.relkind = 'r') as \"table_count!\",\n (select count(*) from pg_class c where c.relnamespace = n.oid and c.relkind = 'v') as \"view_count!\",\n (select count(*) from pg_proc p where p.pronamespace = n.oid) as \"function_count!\",\n\n coalesce(\n (select pg_size_pretty(sum(pg_total_relation_size(c.oid)))\n from pg_class c \n where c.relnamespace = n.oid and c.relkind in ('r', 'i', 'm')),\n '0 bytes'\n) as \"total_size!\"\nfrom\n pg_namespace n,\n pg_roles u\nwhere\n n.nspowner = u.oid\n and (\n pg_has_role(n.nspowner, 'USAGE')\n or has_schema_privilege(n.oid, 'CREATE, USAGE')\n )\n and not pg_catalog.starts_with(n.nspname, 'pg_temp_')\n and not pg_catalog.starts_with(n.nspname, 'pg_toast_temp_');", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id!", + "type_info": "Int8" + }, + { + "ordinal": 1, + "name": "name", + "type_info": "Name" + }, + { + "ordinal": 2, + "name": "owner!", + "type_info": "Name" + }, + { + "ordinal": 3, + "name": "comment", + "type_info": "Text" + }, + { + "ordinal": 4, + "name": "allowed_users!", + "type_info": "TextArray" + }, + { + "ordinal": 5, + "name": "allowed_creators!", + "type_info": "TextArray" + }, + { + "ordinal": 6, + "name": "table_count!", + "type_info": "Int8" + }, + { + "ordinal": 7, + "name": "view_count!", + "type_info": "Int8" + }, + { + "ordinal": 8, + "name": "function_count!", + "type_info": "Int8" + }, + { + "ordinal": 9, + "name": "total_size!", + "type_info": "Text" + } + ], + "parameters": { + "Left": [] + }, + "nullable": [ + null, + false, + true, + null, + null, + null, + null, + null, + null, + null + ] + }, + "hash": "c775d3eaa6e95504de411dd5c2433a1121d69c7dca3c0e5e202835d9fefaa7cc" +} diff --git a/justfile b/justfile index 18b4ed35d..04412b4c9 100644 --- a/justfile +++ b/justfile @@ -108,6 +108,7 @@ ready: cargo run -p xtask_codegen -- configuration cargo run -p docs_codegen cargo run -p xtask_codegen -- bindings + cargo sqlx prepare --workspace just lint-fix just format git diff --exit-code --quiet