Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
134 changes: 130 additions & 4 deletions crates/pgls_hover/src/hoverables/table.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ use pgls_treesitter::TreesitterContext;

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

const MAX_COLUMNS_IN_HOVER: usize = 20;

impl ToHoverMarkdown for Table {
fn hover_headline<W: Write>(
&self,
Expand Down Expand Up @@ -37,15 +39,61 @@ impl ToHoverMarkdown for Table {
fn hover_body<W: Write>(
&self,
writer: &mut W,
_schema_cache: &SchemaCache,
schema_cache: &SchemaCache,
) -> Result<bool, std::fmt::Error> {
if let Some(comment) = &self.comment {
write!(writer, "Comment: '{comment}'")?;
writeln!(writer)?;
Ok(true)
} else {
Ok(false)
}

let mut columns: Vec<_> = schema_cache
.columns
.iter()
.filter(|column| column.schema_name == self.schema && column.table_name == self.name)
.collect();
columns.sort_by_key(|column| column.number);

writeln!(writer, "Columns:")?;

for column in columns.iter().take(MAX_COLUMNS_IN_HOVER) {
write!(writer, "- {}: ", column.name)?;

if let Some(type_name) = &column.type_name {
write!(writer, "{type_name}")?;

if let Some(varchar_length) = column.varchar_length {
write!(writer, "({varchar_length})")?;
}
} else {
write!(writer, "typeid:{}", column.type_id)?;
}

if column.is_nullable {
write!(writer, " - nullable")?;
} else {
write!(writer, " - not null")?;
}

if let Some(default_expr) = column
.default_expr
.as_deref()
.and_then(extract_basic_default_literal)
{
write!(writer, " - default: {default_expr}")?;
}

writeln!(writer)?;
}

if columns.len() > MAX_COLUMNS_IN_HOVER {
writeln!(
writer,
"... +{} more columns",
columns.len() - MAX_COLUMNS_IN_HOVER
)?;
}

Ok(true)
}

fn hover_footer<W: Write>(
Expand All @@ -65,6 +113,45 @@ impl ToHoverMarkdown for Table {
}
}

// `extract_basic_default_literal` will extract simple default literals for table hover.
// Example: `'anonymous'::text` -> `anonymous`, `(42)::int8` -> `42`, `now()` -> ignored.
fn extract_basic_default_literal(default_expr: &str) -> Option<String> {
let mut cast_parts = default_expr.split("::");
let mut value = cast_parts.next().unwrap_or(default_expr).trim();

if cast_parts.any(|cast_part| !is_type_cast_fragment(cast_part)) {
return None;
}

while value.starts_with('(') && value.ends_with(')') && value.len() > 1 {
value = value[1..value.len() - 1].trim();
}

if value.starts_with('\'') && value.ends_with('\'') && value.len() > 1 {
value = &value[1..value.len() - 1];
}

let value = value.trim();
if value.is_empty() || !contains_only_basic_chars(value) {
return None;
}

Some(value.to_string())
}

fn contains_only_basic_chars(value: &str) -> bool {
value
.chars()
.all(|c| c.is_ascii_alphanumeric() || matches!(c, ' ' | '_' | '-' | '.'))
}

fn is_type_cast_fragment(value: &str) -> bool {
value.trim().chars().all(|c| {
c.is_ascii_alphanumeric()
|| matches!(c, ' ' | '_' | '.' | '"' | '[' | ']' | '(' | ')' | ',')
})
}

impl ContextualPriority for Table {
fn relevance_score(&self, ctx: &TreesitterContext) -> f32 {
let mut score = 0.0;
Expand Down Expand Up @@ -93,3 +180,42 @@ impl ContextualPriority for Table {
score
}
}

#[cfg(test)]
mod tests {
use super::extract_basic_default_literal;

#[test]
fn extracts_basic_defaults_with_optional_casts() {
assert_eq!(
extract_basic_default_literal("'anonymous'::text"),
Some("anonymous".to_string())
);
assert_eq!(
extract_basic_default_literal("(42)::int8"),
Some("42".to_string())
);
assert_eq!(
extract_basic_default_literal("NULL::character varying"),
Some("NULL".to_string())
);
assert_eq!(
extract_basic_default_literal("false::boolean"),
Some("false".to_string())
);
}

#[test]
fn ignores_non_basic_defaults() {
assert_eq!(
extract_basic_default_literal("nextval('users_id_seq'::regclass)"),
None
);
assert_eq!(extract_basic_default_literal("now()"), None);
assert_eq!(
extract_basic_default_literal("'a'::text || 'b'::text"),
None
);
assert_eq!(extract_basic_default_literal("'with@symbol'::text"), None);
}
}
43 changes: 43 additions & 0 deletions crates/pgls_hover/tests/hover_integration_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,49 @@ async fn test_table_hover_works(test_db: PgPool) {
test_hover_at_cursor("table_hover", query, Some(setup), &test_db).await;
}

#[sqlx::test(migrator = "pgls_test_utils::MIGRATIONS")]
async fn test_table_hover_select_star(test_db: PgPool) {
let setup = r#"
create table users (
id serial primary key,
email varchar(255) not null
);
"#;

let query = format!(
"select * from use{}rs",
QueryWithCursorPosition::cursor_marker()
);

test_hover_at_cursor("table_hover_select_star", query, Some(setup), &test_db).await;
}

#[sqlx::test(migrator = "pgls_test_utils::MIGRATIONS")]
async fn test_table_hover_shows_nullable_and_basic_defaults(test_db: PgPool) {
let setup = r#"
create table users (
id serial primary key,
name text default 'anonymous',
score int default 0,
enabled bool default false,
created_at timestamptz default now()
);
"#;

let query = format!(
"select * from use{}rs",
QueryWithCursorPosition::cursor_marker()
);

test_hover_at_cursor(
"table_hover_nullable_and_basic_defaults",
query,
Some(setup),
&test_db,
)
.await;
}

#[sqlx::test(migrator = "pgls_test_utils::MIGRATIONS")]
async fn test_no_hover_on_keyword(test_db: PgPool) {
let setup = r#"
Expand Down
5 changes: 4 additions & 1 deletion crates/pgls_hover/tests/snapshots/create_policy.snap
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
source: crates/pgt_hover/tests/hover_integration_tests.rs
source: crates/pgls_hover/tests/hover_integration_tests.rs
expression: snapshot
---
# Input
Expand All @@ -11,6 +11,9 @@ create policy "my cool pol" on users for all to public with check (true);
# Hover Results
### `public.users` - 🔓 RLS disabled
```plain
Columns:
- id: int4 - not null
- name: text - nullable

```
---
Expand Down
5 changes: 4 additions & 1 deletion crates/pgls_hover/tests/snapshots/grant_select.snap
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
source: crates/pgt_hover/tests/hover_integration_tests.rs
source: crates/pgls_hover/tests/hover_integration_tests.rs
expression: snapshot
---
# Input
Expand All @@ -11,6 +11,9 @@ grant select on users to public;
# Hover Results
### `public.users` - 🔓 RLS disabled
```plain
Columns:
- id: int4 - not null
- name: text - nullable

```
---
Expand Down
5 changes: 4 additions & 1 deletion crates/pgls_hover/tests/snapshots/revoke_select.snap
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
source: crates/pgt_hover/tests/hover_integration_tests.rs
source: crates/pgls_hover/tests/hover_integration_tests.rs
expression: snapshot
---
# Input
Expand All @@ -11,6 +11,9 @@ revoke select on users from public;
# Hover Results
### `public.users` - 🔓 RLS disabled
```plain
Columns:
- id: int4 - not null
- name: text - nullable

```
---
Expand Down
5 changes: 4 additions & 1 deletion crates/pgls_hover/tests/snapshots/table_hover.snap
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
source: crates/pgt_hover/tests/hover_integration_tests.rs
source: crates/pgls_hover/tests/hover_integration_tests.rs
expression: snapshot
---
# Input
Expand All @@ -11,6 +11,9 @@ select id from users
# Hover Results
### `public.users` - 🔓 RLS disabled
```plain
Columns:
- id: int4 - not null
- email: varchar(255) - not null

```
---
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
---
source: crates/pgls_hover/tests/hover_integration_tests.rs
expression: snapshot
---
# Input
```sql
select * from users
↑ hovered here
```

# Hover Results
### `public.users` - 🔓 RLS disabled
```plain
Columns:
- id: int4 - not null
- name: text - nullable - default: anonymous
- score: int4 - nullable - default: 0
- enabled: bool - nullable - default: false
- created_at: timestamptz - nullable

```
---
```plain

~0 rows, ~0 dead rows, 16.38 kB
```
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
source: crates/pgt_hover/tests/hover_integration_tests.rs
source: crates/pgls_hover/tests/hover_integration_tests.rs
expression: snapshot
---
# Input
Expand All @@ -11,6 +11,9 @@ select * from "auth".users
# Hover Results
### `auth.users` - 🔓 RLS disabled
```plain
Columns:
- id: int4 - not null
- email: varchar(255) - not null

```
---
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
source: crates/pgt_hover/tests/hover_integration_tests.rs
source: crates/pgls_hover/tests/hover_integration_tests.rs
expression: snapshot
---
# Input
Expand All @@ -11,6 +11,9 @@ select * from "auth"."users"
# Hover Results
### `auth.users` - 🔓 RLS disabled
```plain
Columns:
- id: int4 - not null
- email: varchar(255) - not null

```
---
Expand Down
23 changes: 23 additions & 0 deletions crates/pgls_hover/tests/snapshots/table_hover_select_star.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
---
source: crates/pgls_hover/tests/hover_integration_tests.rs
expression: snapshot
---
# Input
```sql
select * from users
↑ hovered here
```

# Hover Results
### `public.users` - 🔓 RLS disabled
```plain
Columns:
- id: int4 - not null
- email: varchar(255) - not null

```
---
```plain

~0 rows, ~0 dead rows, 8.19 kB
```
13 changes: 0 additions & 13 deletions sample.sql

This file was deleted.

Loading