From e75ff2630cbc98f69fe4ce004f652d1dab981f24 Mon Sep 17 00:00:00 2001 From: psteinroe Date: Sat, 1 Nov 2025 19:01:25 +0100 Subject: [PATCH 1/3] feat(diagnostics): database object location --- crates/pgls_diagnostics/src/context.rs | 3 ++ crates/pgls_diagnostics/src/display.rs | 1 + crates/pgls_diagnostics/src/location.rs | 43 +++++++++++++++++++++ crates/pgls_diagnostics/src/serde.rs | 1 + crates/pgls_diagnostics_macros/src/parse.rs | 4 ++ 5 files changed, 52 insertions(+) diff --git a/crates/pgls_diagnostics/src/context.rs b/crates/pgls_diagnostics/src/context.rs index 6cefb2d11..af85f2d00 100644 --- a/crates/pgls_diagnostics/src/context.rs +++ b/crates/pgls_diagnostics/src/context.rs @@ -458,6 +458,7 @@ mod internal { resource: match loc.resource { Some(Resource::Argv) => Some(Resource::Argv), Some(Resource::Memory) => Some(Resource::Memory), + Some(Resource::Database) => Some(Resource::Database), Some(Resource::File(file)) => { if let Some(Resource::File(path)) = &self.path { Some(Resource::File(path.as_ref())) @@ -468,6 +469,7 @@ mod internal { None => self.path.as_ref().map(Resource::as_deref), }, span: loc.span, + database_object: loc.database_object, source_code: loc.source_code, } } @@ -523,6 +525,7 @@ mod internal { Location { resource: loc.resource, span: self.span.or(loc.span), + database_object: loc.database_object, source_code: loc.source_code, } } diff --git a/crates/pgls_diagnostics/src/display.rs b/crates/pgls_diagnostics/src/display.rs index b19707f89..0ddf2a23d 100644 --- a/crates/pgls_diagnostics/src/display.rs +++ b/crates/pgls_diagnostics/src/display.rs @@ -790,6 +790,7 @@ mod tests { visitor.record_frame(Location { resource: Some(Resource::File("other_path")), span: Some(TextRange::new(TextSize::from(8), TextSize::from(16))), + database_object: None, source_code: Some(SourceCode { text: "context location context", line_starts: None, diff --git a/crates/pgls_diagnostics/src/location.rs b/crates/pgls_diagnostics/src/location.rs index 40597e8ef..f8df7aea7 100644 --- a/crates/pgls_diagnostics/src/location.rs +++ b/crates/pgls_diagnostics/src/location.rs @@ -12,6 +12,8 @@ pub struct Location<'a> { /// An optional range of text within the resource associated with the /// diagnostic. pub span: Option, + /// An optional tuple identifying a database object + pub database_object: Option<(Option<&'a str>, &'a str)>, /// The optional source code of the resource. pub source_code: Option>, } @@ -23,6 +25,7 @@ impl<'a> Location<'a> { resource: None, span: None, source_code: None, + database_object: None, } } } @@ -42,6 +45,8 @@ impl Eq for Location<'_> {} #[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] #[serde(rename_all = "camelCase")] pub enum Resource { + /// The diagnostic is related to the database and its schema. + Database, /// The diagnostic is related to the content of the command line arguments. Argv, /// The diagnostic is related to the content of a memory buffer. @@ -70,6 +75,7 @@ impl

Resource

{ P: Deref, { match self { + Resource::Database => Resource::Database, Resource::Argv => Resource::Argv, Resource::Memory => Resource::Memory, Resource::File(file) => Resource::File(file), @@ -81,6 +87,7 @@ impl Resource<&'_ str> { /// Converts a `Path<&str>` to `Path`. pub fn to_owned(self) -> Resource { match self { + Resource::Database => Resource::Database, Resource::Argv => Resource::Argv, Resource::Memory => Resource::Memory, Resource::File(file) => Resource::File(file.to_owned()), @@ -194,6 +201,7 @@ pub struct LocationBuilder<'a> { resource: Option>, span: Option, source_code: Option>, + database_object: Option<(Option<&'a str>, &'a str)>, } impl<'a> LocationBuilder<'a> { @@ -212,11 +220,17 @@ impl<'a> LocationBuilder<'a> { self } + pub fn database_object(mut self, database_object: &'a D) -> Self { + self.database_object = database_object.as_database_object(); + self + } + pub fn build(self) -> Location<'a> { Location { resource: self.resource, span: self.span, source_code: self.source_code, + database_object: self.database_object, } } } @@ -342,6 +356,35 @@ impl AsSourceCode for String { } } +/// Utility trait for types that can be converted into a database object reference +pub trait AsDatabaseObject { + fn as_database_object(&self) -> Option<(Option<&'_ str>, &'_ str)>; +} + +impl AsDatabaseObject for Option { + fn as_database_object(&self) -> Option<(Option<&'_ str>, &'_ str)> { + self.as_ref().and_then(T::as_database_object) + } +} + +impl AsDatabaseObject for &'_ T { + fn as_database_object(&self) -> Option<(Option<&'_ str>, &'_ str)> { + T::as_database_object(*self) + } +} + +impl AsDatabaseObject for (Option<&str>, &str) { + fn as_database_object(&self) -> Option<(Option<&'_ str>, &'_ str)> { + Some((self.0, self.1)) + } +} + +impl AsDatabaseObject for (Option, String) { + fn as_database_object(&self) -> Option<(Option<&'_ str>, &'_ str)> { + Some((self.0.as_deref(), self.1.as_str())) + } +} + #[cfg(test)] mod tests { use pgls_text_size::TextSize; diff --git a/crates/pgls_diagnostics/src/serde.rs b/crates/pgls_diagnostics/src/serde.rs index 7e0e7a098..437354b21 100644 --- a/crates/pgls_diagnostics/src/serde.rs +++ b/crates/pgls_diagnostics/src/serde.rs @@ -274,6 +274,7 @@ impl super::Advices for Advice { Advice::Frame(location) => visitor.record_frame(super::Location { resource: location.path.as_ref().map(super::Resource::as_deref), span: location.span, + database_object: None, source_code: location.source_code.as_deref().map(|text| SourceCode { text, line_starts: None, diff --git a/crates/pgls_diagnostics_macros/src/parse.rs b/crates/pgls_diagnostics_macros/src/parse.rs index 0e6e1b91e..f013a83ad 100644 --- a/crates/pgls_diagnostics_macros/src/parse.rs +++ b/crates/pgls_diagnostics_macros/src/parse.rs @@ -436,6 +436,7 @@ pub(crate) enum LocationField { Resource(Ident), Span(Ident), SourceCode(Ident), + DatabaseObject(Ident), } impl Parse for LocationAttr { @@ -450,6 +451,8 @@ impl Parse for LocationAttr { LocationField::Span(ident) } else if ident == "source_code" { LocationField::SourceCode(ident) + } else if ident == "database_object" { + LocationField::DatabaseObject(ident) } else { return Err(Error::new_spanned(ident, "unknown location field")); }; @@ -467,6 +470,7 @@ impl ToTokens for LocationField { LocationField::Resource(ident) => ident.to_tokens(tokens), LocationField::Span(ident) => ident.to_tokens(tokens), LocationField::SourceCode(ident) => ident.to_tokens(tokens), + LocationField::DatabaseObject(ident) => ident.to_tokens(tokens), } } } From 5c7aa785517351c76146e6f69aae1ea539f4cb3d Mon Sep 17 00:00:00 2001 From: psteinroe Date: Sat, 1 Nov 2025 19:24:50 +0100 Subject: [PATCH 2/3] progress --- crates/pgls_diagnostics/src/display.rs | 17 +++++++++ crates/pgls_diagnostics/src/lib.rs | 4 +- crates/pgls_diagnostics/src/location.rs | 50 +++++++++++++++++++------ crates/pgls_diagnostics/src/serde.rs | 27 +++++++++++++ 4 files changed, 85 insertions(+), 13 deletions(-) diff --git a/crates/pgls_diagnostics/src/display.rs b/crates/pgls_diagnostics/src/display.rs index 0ddf2a23d..cacc459fa 100644 --- a/crates/pgls_diagnostics/src/display.rs +++ b/crates/pgls_diagnostics/src/display.rs @@ -139,6 +139,23 @@ impl fmt::Display for PrintHeader<'_, D> { fmt.write_str(" ")?; } + // Print the database object if present (e.g., "table public.contacts") + if let Some(db_obj) = location.database_object { + if let Some(obj_type) = db_obj.object_type { + fmt.write_markup(markup! { + {obj_type}" " + })?; + } + if let Some(schema) = db_obj.schema { + fmt.write_markup(markup! { + {schema}"." + })?; + } + fmt.write_markup(markup! { + {db_obj.name}" " + })?; + } + // Print the category of the diagnostic, with a hyperlink if // the category has an associated link if let Some(category) = diagnostic.category() { diff --git a/crates/pgls_diagnostics/src/lib.rs b/crates/pgls_diagnostics/src/lib.rs index ffb9f5f58..add7e1449 100644 --- a/crates/pgls_diagnostics/src/lib.rs +++ b/crates/pgls_diagnostics/src/lib.rs @@ -36,7 +36,9 @@ pub use crate::display::{ }; pub use crate::display_github::PrintGitHubDiagnostic; pub use crate::error::{Error, Result}; -pub use crate::location::{LineIndex, LineIndexBuf, Location, Resource, SourceCode}; +pub use crate::location::{ + DatabaseObject, DatabaseObjectOwned, LineIndex, LineIndexBuf, Location, Resource, SourceCode, +}; use pgls_console::fmt::{Formatter, Termcolor}; use pgls_console::markup; use std::fmt::Write; diff --git a/crates/pgls_diagnostics/src/location.rs b/crates/pgls_diagnostics/src/location.rs index f8df7aea7..04a49af1e 100644 --- a/crates/pgls_diagnostics/src/location.rs +++ b/crates/pgls_diagnostics/src/location.rs @@ -4,6 +4,28 @@ use std::fmt::Debug; use std::ops::Range; use std::{borrow::Borrow, ops::Deref}; +/// Represents a database object (table, function, etc.) +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct DatabaseObject<'a> { + /// Optional schema name + pub schema: Option<&'a str>, + /// Object name (required) + pub name: &'a str, + /// Optional object type (e.g., "table", "function", "view") + pub object_type: Option<&'a str>, +} + +/// Owned version of DatabaseObject for use in diagnostic structs +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct DatabaseObjectOwned { + /// Optional schema name + pub schema: Option, + /// Object name (required) + pub name: String, + /// Optional object type (e.g., "table", "function", "view") + pub object_type: Option, +} + /// Represents the location of a diagnostic in a resource. #[derive(Debug, Default, Clone, Copy)] pub struct Location<'a> { @@ -12,8 +34,8 @@ pub struct Location<'a> { /// An optional range of text within the resource associated with the /// diagnostic. pub span: Option, - /// An optional tuple identifying a database object - pub database_object: Option<(Option<&'a str>, &'a str)>, + /// An optional database object reference + pub database_object: Option>, /// The optional source code of the resource. pub source_code: Option>, } @@ -201,7 +223,7 @@ pub struct LocationBuilder<'a> { resource: Option>, span: Option, source_code: Option>, - database_object: Option<(Option<&'a str>, &'a str)>, + database_object: Option>, } impl<'a> LocationBuilder<'a> { @@ -358,30 +380,34 @@ impl AsSourceCode for String { /// Utility trait for types that can be converted into a database object reference pub trait AsDatabaseObject { - fn as_database_object(&self) -> Option<(Option<&'_ str>, &'_ str)>; + fn as_database_object(&self) -> Option>; } impl AsDatabaseObject for Option { - fn as_database_object(&self) -> Option<(Option<&'_ str>, &'_ str)> { + fn as_database_object(&self) -> Option> { self.as_ref().and_then(T::as_database_object) } } impl AsDatabaseObject for &'_ T { - fn as_database_object(&self) -> Option<(Option<&'_ str>, &'_ str)> { + fn as_database_object(&self) -> Option> { T::as_database_object(*self) } } -impl AsDatabaseObject for (Option<&str>, &str) { - fn as_database_object(&self) -> Option<(Option<&'_ str>, &'_ str)> { - Some((self.0, self.1)) +impl<'a> AsDatabaseObject for DatabaseObject<'a> { + fn as_database_object(&self) -> Option> { + Some(*self) } } -impl AsDatabaseObject for (Option, String) { - fn as_database_object(&self) -> Option<(Option<&'_ str>, &'_ str)> { - Some((self.0.as_deref(), self.1.as_str())) +impl AsDatabaseObject for DatabaseObjectOwned { + fn as_database_object(&self) -> Option> { + Some(DatabaseObject { + schema: self.schema.as_deref(), + name: &self.name, + object_type: self.object_type.as_deref(), + }) } } diff --git a/crates/pgls_diagnostics/src/serde.rs b/crates/pgls_diagnostics/src/serde.rs index 437354b21..ab57d90fa 100644 --- a/crates/pgls_diagnostics/src/serde.rs +++ b/crates/pgls_diagnostics/src/serde.rs @@ -497,4 +497,31 @@ mod tests { // // assert_eq!(diag, expected); // } + + #[test] + fn test_database_object_location_macro() { + use crate::{DatabaseObjectOwned, Diagnostic}; + + #[derive(Debug, Diagnostic)] + #[diagnostic(severity = Error, category = "lint")] + struct TestDatabaseObjectDiagnostic { + #[location(database_object)] + db_object: DatabaseObjectOwned, + } + + let diag = TestDatabaseObjectDiagnostic { + db_object: DatabaseObjectOwned { + schema: Some("public".to_string()), + name: "contacts".to_string(), + object_type: Some("table".to_string()), + }, + }; + + let location = diag.location(); + assert!(location.database_object.is_some()); + let db_obj = location.database_object.unwrap(); + assert_eq!(db_obj.schema, Some("public")); + assert_eq!(db_obj.name, "contacts"); + assert_eq!(db_obj.object_type, Some("table")); + } } From ba5d256fd499c9e99a3ff8c3d8485621bd03e9e1 Mon Sep 17 00:00:00 2001 From: psteinroe Date: Sat, 1 Nov 2025 19:56:08 +0100 Subject: [PATCH 3/3] progress --- .../backend-jsonrpc/src/workspace.ts | 6 +++++- packages/@postgrestools/backend-jsonrpc/src/workspace.ts | 6 +++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/packages/@postgres-language-server/backend-jsonrpc/src/workspace.ts b/packages/@postgres-language-server/backend-jsonrpc/src/workspace.ts index 701554f89..5c0aa4f1f 100644 --- a/packages/@postgres-language-server/backend-jsonrpc/src/workspace.ts +++ b/packages/@postgres-language-server/backend-jsonrpc/src/workspace.ts @@ -139,7 +139,11 @@ export type Advice = /** * Represents the resource a diagnostic is associated with. */ -export type Resource_for_String = "argv" | "memory" | { file: string }; +export type Resource_for_String = + | "database" + | "argv" + | "memory" + | { file: string }; export type TextRange = [TextSize, TextSize]; export interface MarkupNodeBuf { content: string; diff --git a/packages/@postgrestools/backend-jsonrpc/src/workspace.ts b/packages/@postgrestools/backend-jsonrpc/src/workspace.ts index 701554f89..5c0aa4f1f 100644 --- a/packages/@postgrestools/backend-jsonrpc/src/workspace.ts +++ b/packages/@postgrestools/backend-jsonrpc/src/workspace.ts @@ -139,7 +139,11 @@ export type Advice = /** * Represents the resource a diagnostic is associated with. */ -export type Resource_for_String = "argv" | "memory" | { file: string }; +export type Resource_for_String = + | "database" + | "argv" + | "memory" + | { file: string }; export type TextRange = [TextSize, TextSize]; export interface MarkupNodeBuf { content: string;