From ca5fd1dfd8d8aaaa1b2f99fed192f4ef97df44b9 Mon Sep 17 00:00:00 2001 From: psteinroe Date: Thu, 4 Sep 2025 09:03:52 +0200 Subject: [PATCH 1/3] feat: disable settings --- crates/pgt_configuration/src/lib.rs | 12 ++ crates/pgt_configuration/src/plpgsql_check.rs | 20 ++ crates/pgt_configuration/src/typecheck.rs | 4 + crates/pgt_workspace/src/settings.rs | 33 +++ crates/pgt_workspace/src/workspace/server.rs | 186 +++++++++-------- .../src/workspace/server.tests.rs | 195 +++++++++++++++++- 6 files changed, 365 insertions(+), 85 deletions(-) create mode 100644 crates/pgt_configuration/src/plpgsql_check.rs diff --git a/crates/pgt_configuration/src/lib.rs b/crates/pgt_configuration/src/lib.rs index 2b8db652..8cac0c3f 100644 --- a/crates/pgt_configuration/src/lib.rs +++ b/crates/pgt_configuration/src/lib.rs @@ -9,6 +9,7 @@ pub mod diagnostics; pub mod files; pub mod generated; pub mod migrations; +pub mod plpgsql_check; pub mod typecheck; pub mod vcs; @@ -33,6 +34,10 @@ use files::{FilesConfiguration, PartialFilesConfiguration, partial_files_configu use migrations::{ MigrationsConfiguration, PartialMigrationsConfiguration, partial_migrations_configuration, }; +use plpgsql_check::{ + PartialPlPgSqlCheckConfiguration, PlPgSqlCheckConfiguration, + partial_pl_pg_sql_check_configuration, +}; use serde::{Deserialize, Serialize}; pub use typecheck::{ PartialTypecheckConfiguration, TypecheckConfiguration, partial_typecheck_configuration, @@ -85,6 +90,10 @@ pub struct Configuration { #[partial(type, bpaf(external(partial_typecheck_configuration), optional))] pub typecheck: TypecheckConfiguration, + /// The configuration for type checking + #[partial(type, bpaf(external(partial_pl_pg_sql_check_configuration), optional))] + pub plpgsql_check: PlPgSqlCheckConfiguration, + /// The configuration of the database connection #[partial( type, @@ -121,6 +130,9 @@ impl PartialConfiguration { typecheck: Some(PartialTypecheckConfiguration { ..Default::default() }), + plpgsql_check: Some(PartialPlPgSqlCheckConfiguration { + ..Default::default() + }), db: Some(PartialDatabaseConfiguration { host: Some("127.0.0.1".to_string()), port: Some(5432), diff --git a/crates/pgt_configuration/src/plpgsql_check.rs b/crates/pgt_configuration/src/plpgsql_check.rs new file mode 100644 index 00000000..fd2fcfd9 --- /dev/null +++ b/crates/pgt_configuration/src/plpgsql_check.rs @@ -0,0 +1,20 @@ +use biome_deserialize_macros::{Merge, Partial}; +use bpaf::Bpaf; +use serde::{Deserialize, Serialize}; + +/// The configuration for type checking. +#[derive(Clone, Debug, Deserialize, Eq, Partial, PartialEq, Serialize)] +#[partial(derive(Bpaf, Clone, Eq, PartialEq, Merge))] +#[partial(cfg_attr(feature = "schema", derive(schemars::JsonSchema)))] +#[partial(serde(rename_all = "camelCase", default, deny_unknown_fields))] +pub struct PlPgSqlCheckConfiguration { + /// if `false`, it disables the feature and pglpgsql_check won't be executed. `true` by default + #[partial(bpaf(hide))] + pub enabled: bool, +} + +impl Default for PlPgSqlCheckConfiguration { + fn default() -> Self { + Self { enabled: true } + } +} diff --git a/crates/pgt_configuration/src/typecheck.rs b/crates/pgt_configuration/src/typecheck.rs index 32c39377..bff96d81 100644 --- a/crates/pgt_configuration/src/typecheck.rs +++ b/crates/pgt_configuration/src/typecheck.rs @@ -9,6 +9,9 @@ use serde::{Deserialize, Serialize}; #[partial(cfg_attr(feature = "schema", derive(schemars::JsonSchema)))] #[partial(serde(rename_all = "camelCase", default, deny_unknown_fields))] pub struct TypecheckConfiguration { + /// if `false`, it disables the feature and the typechecker won't be executed. `true` by default + #[partial(bpaf(hide))] + pub enabled: bool, /// Default search path schemas for type checking. /// Can be a list of schema names or glob patterns like ["public", "app_*"]. /// If not specified, defaults to ["public"]. @@ -19,6 +22,7 @@ pub struct TypecheckConfiguration { impl Default for TypecheckConfiguration { fn default() -> Self { Self { + enabled: true, search_path: ["public".to_string()].into_iter().collect(), } } diff --git a/crates/pgt_workspace/src/settings.rs b/crates/pgt_workspace/src/settings.rs index 5561625d..46543980 100644 --- a/crates/pgt_workspace/src/settings.rs +++ b/crates/pgt_workspace/src/settings.rs @@ -17,6 +17,7 @@ use pgt_configuration::{ diagnostics::InvalidIgnorePattern, files::FilesConfiguration, migrations::{MigrationsConfiguration, PartialMigrationsConfiguration}, + plpgsql_check::PlPgSqlCheckConfiguration, }; use pgt_fs::PgTPath; @@ -213,6 +214,9 @@ pub struct Settings { /// Type checking settings for the workspace pub typecheck: TypecheckSettings, + /// plpgsql_check settings for the workspace + pub plpgsql_check: PlPgSqlCheckSettings, + /// Migrations settings pub migrations: Option, } @@ -253,6 +257,12 @@ impl Settings { self.typecheck = to_typecheck_settings(TypecheckConfiguration::from(typecheck)); } + // plpgsql_check part + if let Some(plpgsql_check) = configuration.plpgsql_check { + self.plpgsql_check = + to_plpgsql_check_settings(PlPgSqlCheckConfiguration::from(plpgsql_check)); + } + // Migrations settings if let Some(migrations) = configuration.migrations { self.migrations = to_migration_settings( @@ -305,6 +315,13 @@ fn to_linter_settings( fn to_typecheck_settings(conf: TypecheckConfiguration) -> TypecheckSettings { TypecheckSettings { search_path: conf.search_path.into_iter().collect(), + enabled: conf.enabled, + } +} + +fn to_plpgsql_check_settings(conf: PlPgSqlCheckConfiguration) -> PlPgSqlCheckSettings { + PlPgSqlCheckSettings { + enabled: conf.enabled, } } @@ -415,9 +432,24 @@ impl Default for LinterSettings { } } +/// Type checking settings for the entire workspace +#[derive(Debug)] +pub struct PlPgSqlCheckSettings { + /// Enabled by default + pub enabled: bool, +} + +impl Default for PlPgSqlCheckSettings { + fn default() -> Self { + Self { enabled: true } + } +} + /// Type checking settings for the entire workspace #[derive(Debug)] pub struct TypecheckSettings { + /// Enabled by default + pub enabled: bool, /// Default search path schemas for type checking pub search_path: Vec, } @@ -425,6 +457,7 @@ pub struct TypecheckSettings { impl Default for TypecheckSettings { fn default() -> Self { Self { + enabled: true, search_path: vec!["public".to_string()], } } diff --git a/crates/pgt_workspace/src/workspace/server.rs b/crates/pgt_workspace/src/workspace/server.rs index 7a4abbdf..6812c246 100644 --- a/crates/pgt_workspace/src/workspace/server.rs +++ b/crates/pgt_workspace/src/workspace/server.rs @@ -451,93 +451,111 @@ impl Workspace for WorkspaceServer { /* * Type-checking against database connection */ - if let Some(pool) = self.get_current_connection() { - let path_clone = params.path.clone(); - let schema_cache = self.schema_cache.load(pool.clone())?; - let input = doc.iter(TypecheckDiagnosticsMapper).collect::>(); - let search_path_patterns = settings.typecheck.search_path.clone(); - - // Combined async context for both typecheck and plpgsql_check - let async_results = run_async(async move { - stream::iter(input) - .map(|(id, range, ast, cst, sign)| { - let pool = pool.clone(); - let path = path_clone.clone(); - let schema_cache = Arc::clone(&schema_cache); - let search_path_patterns = search_path_patterns.clone(); - - async move { - let mut diagnostics = Vec::new(); - - if let Some(ast) = ast { - // Type checking - let typecheck_result = pgt_typecheck::check_sql(TypecheckParams { - conn: &pool, - sql: convert_to_positional_params(id.content()).as_str(), - ast: &ast, - tree: &cst, - schema_cache: schema_cache.as_ref(), - search_path_patterns, - identifiers: sign - .map(|s| { - s.args - .iter() - .map(|a| TypedIdentifier { - path: s.name.clone(), - name: a.name.clone(), - type_: IdentifierType { - schema: a.type_.schema.clone(), - name: a.type_.name.clone(), - is_array: a.type_.is_array, - }, - }) - .collect::>() - }) - .unwrap_or_default(), - }) - .await; - - if let Ok(Some(diag)) = typecheck_result { - let r = diag.location().span.map(|span| span + range.start()); - diagnostics.push( - diag.with_file_path(path.as_path().display().to_string()) - .with_file_span(r.unwrap_or(range)), - ); + let typecheck_enabled = settings.typecheck.enabled; + let plpgsql_check_enabled = settings.plpgsql_check.enabled; + if typecheck_enabled || plpgsql_check_enabled { + if let Some(pool) = self.get_current_connection() { + let path_clone = params.path.clone(); + let schema_cache = self.schema_cache.load(pool.clone())?; + let input = doc.iter(TypecheckDiagnosticsMapper).collect::>(); + let search_path_patterns = settings.typecheck.search_path.clone(); + + // Combined async context for both typecheck and plpgsql_check + let async_results = run_async(async move { + stream::iter(input) + .map(|(id, range, ast, cst, sign)| { + let pool = pool.clone(); + let path = path_clone.clone(); + let schema_cache = Arc::clone(&schema_cache); + let search_path_patterns = search_path_patterns.clone(); + + async move { + let mut diagnostics = Vec::new(); + + if let Some(ast) = ast { + // Type checking + if typecheck_enabled { + let typecheck_result = + pgt_typecheck::check_sql(TypecheckParams { + conn: &pool, + sql: convert_to_positional_params(id.content()) + .as_str(), + ast: &ast, + tree: &cst, + schema_cache: schema_cache.as_ref(), + search_path_patterns, + identifiers: sign + .map(|s| { + s.args + .iter() + .map(|a| TypedIdentifier { + path: s.name.clone(), + name: a.name.clone(), + type_: IdentifierType { + schema: a.type_.schema.clone(), + name: a.type_.name.clone(), + is_array: a.type_.is_array, + }, + }) + .collect::>() + }) + .unwrap_or_default(), + }) + .await; + + if let Ok(Some(diag)) = typecheck_result { + let r = diag + .location() + .span + .map(|span| span + range.start()); + diagnostics.push( + diag.with_file_path( + path.as_path().display().to_string(), + ) + .with_file_span(r.unwrap_or(range)), + ); + } + } + + // plpgsql_check + if plpgsql_check_enabled { + let plpgsql_check_results = + pgt_plpgsql_check::check_plpgsql( + pgt_plpgsql_check::PlPgSqlCheckParams { + conn: &pool, + sql: id.content(), + ast: &ast, + schema_cache: schema_cache.as_ref(), + }, + ) + .await + .unwrap_or_else(|_| vec![]); + + for d in plpgsql_check_results { + let r = d.span.map(|span| span + range.start()); + diagnostics.push( + d.with_file_path( + path.as_path().display().to_string(), + ) + .with_file_span(r.unwrap_or(range)), + ); + } + } } - // plpgsql_check - let plpgsql_check_results = pgt_plpgsql_check::check_plpgsql( - pgt_plpgsql_check::PlPgSqlCheckParams { - conn: &pool, - sql: id.content(), - ast: &ast, - schema_cache: schema_cache.as_ref(), - }, - ) - .await - .unwrap_or_else(|_| vec![]); - - for d in plpgsql_check_results { - let r = d.span.map(|span| span + range.start()); - diagnostics.push( - d.with_file_path(path.as_path().display().to_string()) - .with_file_span(r.unwrap_or(range)), - ); - } + Ok::, sqlx::Error>(diagnostics) } - - Ok::, sqlx::Error>(diagnostics) - } - }) - .buffer_unordered(10) - .collect::>() - .await - })?; - - for result in async_results.into_iter() { - let diagnostics_batch = result?; - for diag in diagnostics_batch { - diagnostics.push(SDiagnostic::new(diag)); + }) + .buffer_unordered(10) + .collect::>() + .await + })?; + + for result in async_results.into_iter() { + let diagnostics_batch = result?; + for diag in diagnostics_batch { + diagnostics.push(SDiagnostic::new(diag)); + } } } } diff --git a/crates/pgt_workspace/src/workspace/server.tests.rs b/crates/pgt_workspace/src/workspace/server.tests.rs index 33520cbf..42eddfe9 100644 --- a/crates/pgt_workspace/src/workspace/server.tests.rs +++ b/crates/pgt_workspace/src/workspace/server.tests.rs @@ -4,7 +4,7 @@ use biome_deserialize::{Merge, StringSet}; use pgt_analyse::RuleCategories; use pgt_configuration::{ PartialConfiguration, PartialTypecheckConfiguration, database::PartialDatabaseConfiguration, - files::PartialFilesConfiguration, + files::PartialFilesConfiguration, plpgsql_check::PartialPlPgSqlCheckConfiguration, }; use pgt_diagnostics::Diagnostic; use pgt_fs::PgTPath; @@ -386,6 +386,198 @@ async fn test_positional_params(test_db: PgPool) { assert_eq!(diagnostics.len(), 0, "Expected no diagnostic"); } +#[sqlx::test(migrator = "pgt_test_utils::MIGRATIONS")] +async fn test_disable_plpgsql_check(test_db: PgPool) { + let mut conf = PartialConfiguration::init(); + conf.merge_with(PartialConfiguration { + db: Some(PartialDatabaseConfiguration { + database: Some( + test_db + .connect_options() + .get_database() + .unwrap() + .to_string(), + ), + ..Default::default() + }), + ..Default::default() + }); + + let workspace = get_test_workspace(Some(conf)).expect("Unable to create test workspace"); + + let path = PgTPath::new("test.sql"); + + let setup_sql = "CREATE EXTENSION IF NOT EXISTS plpgsql_check;"; + test_db.execute(setup_sql).await.expect("setup sql failed"); + + let content = r#" + CREATE OR REPLACE FUNCTION public.f1() + RETURNS void + LANGUAGE plpgsql + AS $function$ + decare r text; + BEGIN + select '1' into into r; + END; + $function$; + "#; + + test_db.execute(setup_sql).await.expect("setup sql failed"); + + workspace + .open_file(OpenFileParams { + path: path.clone(), + content: content.into(), + version: 1, + }) + .expect("Unable to open test file"); + + let diagnostics = workspace + .pull_diagnostics(crate::workspace::PullDiagnosticsParams { + path: path.clone(), + categories: RuleCategories::all(), + max_diagnostics: 100, + only: vec![], + skip: vec![], + }) + .expect("Unable to pull diagnostics") + .diagnostics; + + assert_eq!( + diagnostics + .iter() + .filter(|d| d.category().is_some_and(|c| c.name() == "plpgsql_check")) + .count(), + 1, + "Expected one plpgsql_check diagnostic" + ); + + let _ = workspace.update_settings(UpdateSettingsParams { + configuration: PartialConfiguration { + plpgsql_check: Some(PartialPlPgSqlCheckConfiguration { + enabled: Some(false), + }), + ..Default::default() + }, + gitignore_matches: vec![], + vcs_base_path: None, + workspace_directory: None, + }); + + let diagnostics = workspace + .pull_diagnostics(crate::workspace::PullDiagnosticsParams { + path: path.clone(), + categories: RuleCategories::all(), + max_diagnostics: 100, + only: vec![], + skip: vec![], + }) + .expect("Unable to pull diagnostics") + .diagnostics; + + assert_eq!( + diagnostics + .iter() + .filter(|d| d.category().is_some_and(|c| c.name() == "plpgsql_check")) + .count(), + 0, + "Expected no plpgsql_check diagnostic" + ); +} + +#[sqlx::test(migrator = "pgt_test_utils::MIGRATIONS")] +async fn test_disable_typecheck(test_db: PgPool) { + let mut conf = PartialConfiguration::init(); + conf.merge_with(PartialConfiguration { + db: Some(PartialDatabaseConfiguration { + database: Some( + test_db + .connect_options() + .get_database() + .unwrap() + .to_string(), + ), + ..Default::default() + }), + ..Default::default() + }); + + let workspace = get_test_workspace(Some(conf)).expect("Unable to create test workspace"); + + let path = PgTPath::new("test.sql"); + + let setup_sql = r" + create table users ( + id serial primary key, + email text not null + ); + "; + test_db.execute(setup_sql).await.expect("setup sql failed"); + + let content = r#"select name from users where id = 1;"#; + + workspace + .open_file(OpenFileParams { + path: path.clone(), + content: content.into(), + version: 1, + }) + .expect("Unable to open test file"); + + let diagnostics = workspace + .pull_diagnostics(crate::workspace::PullDiagnosticsParams { + path: path.clone(), + categories: RuleCategories::all(), + max_diagnostics: 100, + only: vec![], + skip: vec![], + }) + .expect("Unable to pull diagnostics") + .diagnostics; + + assert_eq!( + diagnostics + .iter() + .filter(|d| d.category().is_some_and(|c| c.name() == "typecheck")) + .count(), + 1, + "Expected one typecheck diagnostic" + ); + + let _ = workspace.update_settings(UpdateSettingsParams { + configuration: PartialConfiguration { + typecheck: Some(PartialTypecheckConfiguration { + enabled: Some(false), + ..Default::default() + }), + ..Default::default() + }, + gitignore_matches: vec![], + vcs_base_path: None, + workspace_directory: None, + }); + + let diagnostics = workspace + .pull_diagnostics(crate::workspace::PullDiagnosticsParams { + path: path.clone(), + categories: RuleCategories::all(), + max_diagnostics: 100, + only: vec![], + skip: vec![], + }) + .expect("Unable to pull diagnostics") + .diagnostics; + + assert_eq!( + diagnostics + .iter() + .filter(|d| d.category().is_some_and(|c| c.name() == "typecheck")) + .count(), + 0, + "Expected no typecheck diagnostic" + ); +} + #[sqlx::test(migrator = "pgt_test_utils::MIGRATIONS")] async fn test_cstyle_comments(test_db: PgPool) { let mut conf = PartialConfiguration::init(); @@ -513,6 +705,7 @@ async fn test_search_path_configuration(test_db: PgPool) { // adding the glob glob_conf.merge_with(PartialConfiguration { typecheck: Some(PartialTypecheckConfiguration { + enabled: Some(true), // Adding glob pattern to match the "private" schema search_path: Some(StringSet::from_iter(vec!["pr*".to_string()])), }), From 253111c22963c7f1ed115a1a4f71536824cbb5d3 Mon Sep 17 00:00:00 2001 From: psteinroe Date: Thu, 4 Sep 2025 09:29:44 +0200 Subject: [PATCH 2/3] progress --- docs/schema.json | 32 +++++++++++++++++++ .../backend-jsonrpc/src/workspace.ts | 17 ++++++++++ 2 files changed, 49 insertions(+) diff --git a/docs/schema.json b/docs/schema.json index e0abf20a..bf9482ac 100644 --- a/docs/schema.json +++ b/docs/schema.json @@ -66,6 +66,17 @@ } ] }, + "plpgsqlCheck": { + "description": "The configuration for type checking", + "anyOf": [ + { + "$ref": "#/definitions/PlPgSqlCheckConfiguration" + }, + { + "type": "null" + } + ] + }, "typecheck": { "description": "The configuration for type checking", "anyOf": [ @@ -261,6 +272,20 @@ }, "additionalProperties": false }, + "PlPgSqlCheckConfiguration": { + "description": "The configuration for type checking.", + "type": "object", + "properties": { + "enabled": { + "description": "if `false`, it disables the feature and pglpgsql_check won't be executed. `true` by default", + "type": [ + "boolean", + "null" + ] + } + }, + "additionalProperties": false + }, "RuleConfiguration": { "anyOf": [ { @@ -425,6 +450,13 @@ "description": "The configuration for type checking.", "type": "object", "properties": { + "enabled": { + "description": "if `false`, it disables the feature and the typechecker won't be executed. `true` by default", + "type": [ + "boolean", + "null" + ] + }, "searchPath": { "description": "Default search path schemas for type checking. Can be a list of schema names or glob patterns like [\"public\", \"app_*\"]. If not specified, defaults to [\"public\"].", "anyOf": [ diff --git a/packages/@postgrestools/backend-jsonrpc/src/workspace.ts b/packages/@postgrestools/backend-jsonrpc/src/workspace.ts index a8ea3e9f..6f7b3620 100644 --- a/packages/@postgrestools/backend-jsonrpc/src/workspace.ts +++ b/packages/@postgrestools/backend-jsonrpc/src/workspace.ts @@ -256,6 +256,10 @@ export interface PartialConfiguration { * Configure migrations */ migrations?: PartialMigrationsConfiguration; + /** + * The configuration for type checking + */ + plpgsqlCheck?: PartialPlPgSqlCheckConfiguration; /** * The configuration for type checking */ @@ -344,10 +348,23 @@ export interface PartialMigrationsConfiguration { */ migrationsDir?: string; } +/** + * The configuration for type checking. + */ +export interface PartialPlPgSqlCheckConfiguration { + /** + * if `false`, it disables the feature and pglpgsql_check won't be executed. `true` by default + */ + enabled?: boolean; +} /** * The configuration for type checking. */ export interface PartialTypecheckConfiguration { + /** + * if `false`, it disables the feature and the typechecker won't be executed. `true` by default + */ + enabled?: boolean; /** * Default search path schemas for type checking. Can be a list of schema names or glob patterns like ["public", "app_*"]. If not specified, defaults to ["public"]. */ From 848b715e2cca80e70514c7fc2621d6c9a12466ee Mon Sep 17 00:00:00 2001 From: psteinroe Date: Thu, 4 Sep 2025 10:16:07 +0200 Subject: [PATCH 3/3] progress --- crates/pgt_workspace/src/workspace/server.tests.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/pgt_workspace/src/workspace/server.tests.rs b/crates/pgt_workspace/src/workspace/server.tests.rs index 42eddfe9..71a9012c 100644 --- a/crates/pgt_workspace/src/workspace/server.tests.rs +++ b/crates/pgt_workspace/src/workspace/server.tests.rs @@ -386,6 +386,7 @@ async fn test_positional_params(test_db: PgPool) { assert_eq!(diagnostics.len(), 0, "Expected no diagnostic"); } +#[cfg(all(test, not(target_os = "windows")))] #[sqlx::test(migrator = "pgt_test_utils::MIGRATIONS")] async fn test_disable_plpgsql_check(test_db: PgPool) { let mut conf = PartialConfiguration::init();