From 7fef0744a28febc8188c77613e6c2032066d1f57 Mon Sep 17 00:00:00 2001 From: psteinroe Date: Mon, 1 Sep 2025 09:12:54 +0200 Subject: [PATCH 1/2] fix: ignore false positive --- Cargo.lock | 1 + crates/pgt_plpgsql_check/src/lib.rs | 33 ++++++++++++ crates/pgt_workspace/Cargo.toml | 1 + .../src/workspace/server.tests.rs | 53 +++++++++++++++++++ .../src/workspace/server/pg_query.rs | 20 ++++++- 5 files changed, 106 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c085f8449..acd53e983 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3224,6 +3224,7 @@ dependencies = [ "pgt_tokenizer", "pgt_typecheck", "pgt_workspace_macros", + "regex", "rustc-hash 2.1.0", "schemars", "serde", diff --git a/crates/pgt_plpgsql_check/src/lib.rs b/crates/pgt_plpgsql_check/src/lib.rs index 05e2f5709..8f0d4c532 100644 --- a/crates/pgt_plpgsql_check/src/lib.rs +++ b/crates/pgt_plpgsql_check/src/lib.rs @@ -271,6 +271,39 @@ mod tests { Ok((diagnostics, span_texts)) } + #[sqlx::test(migrator = "pgt_test_utils::MIGRATIONS")] + async fn test_plpgsql_check_composite_types(test_db: PgPool) { + let setup = r#" + create extension if not exists plpgsql_check; + + create table if not exists _fetch_cycle_continuation_data ( + next_id bigint, + next_state jsonb null default '{}'::jsonb + constraint abstract_no_data check(false) no inherit + ); + "#; + + let create_fn_sql = r#" + create or replace function continue_fetch_cycle_prototype ( + ) returns _fetch_cycle_continuation_data language plpgsql as $prototype$ + declare + result _fetch_cycle_continuation_data := null; + begin + result.next_id := 0; + result.next_state := '{}'::jsonb; + + return result; + end; + $prototype$; + "#; + + let (diagnostics, span_texts) = run_plpgsql_check_test(&test_db, setup, create_fn_sql) + .await + .expect("Failed to run plpgsql_check test"); + + assert_eq!(diagnostics.len(), 0); + } + #[sqlx::test(migrator = "pgt_test_utils::MIGRATIONS")] async fn test_plpgsql_check_if_expr(test_db: PgPool) { let setup = r#" diff --git a/crates/pgt_workspace/Cargo.toml b/crates/pgt_workspace/Cargo.toml index 397e41a4d..5072eb0a9 100644 --- a/crates/pgt_workspace/Cargo.toml +++ b/crates/pgt_workspace/Cargo.toml @@ -37,6 +37,7 @@ pgt_text_size.workspace = true pgt_tokenizer = { workspace = true } pgt_typecheck = { workspace = true } pgt_workspace_macros = { workspace = true } +regex = { workspace = true } rustc-hash = { workspace = true } schemars = { workspace = true, optional = true } serde = { workspace = true, features = ["derive"] } diff --git a/crates/pgt_workspace/src/workspace/server.tests.rs b/crates/pgt_workspace/src/workspace/server.tests.rs index f1b3810cc..6f8e21bb6 100644 --- a/crates/pgt_workspace/src/workspace/server.tests.rs +++ b/crates/pgt_workspace/src/workspace/server.tests.rs @@ -279,6 +279,59 @@ async fn test_dedupe_diagnostics(test_db: PgPool) { ); } +#[sqlx::test(migrator = "pgt_test_utils::MIGRATIONS")] +async fn test_plpgsql_assign_composite_types(test_db: PgPool) { + let conf = PartialConfiguration::init(); + + 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 if not exists _fetch_cycle_continuation_data ( + next_id bigint, + next_state jsonb null default '{}'::jsonb + constraint abstract_no_data check(false) no inherit + ); + "; + test_db.execute(setup_sql).await.expect("setup sql failed"); + + let content = r#" + create or replace function continue_fetch_cycle_prototype () + returns _fetch_cycle_continuation_data language plpgsql as $prototype$ + declare + result _fetch_cycle_continuation_data := null; + begin + result.next_id := 0; + result.next_state := '{}'::jsonb + + return result; + end; + $prototype$ + "#; + + 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.len(), 0, "Expected no diagnostic"); +} + #[sqlx::test(migrator = "pgt_test_utils::MIGRATIONS")] async fn test_positional_params(test_db: PgPool) { let mut conf = PartialConfiguration::init(); diff --git a/crates/pgt_workspace/src/workspace/server/pg_query.rs b/crates/pgt_workspace/src/workspace/server/pg_query.rs index bd9ffdfce..839bd64c3 100644 --- a/crates/pgt_workspace/src/workspace/server/pg_query.rs +++ b/crates/pgt_workspace/src/workspace/server/pg_query.rs @@ -1,11 +1,12 @@ use std::collections::HashMap; use std::num::NonZeroUsize; -use std::sync::{Arc, Mutex}; +use std::sync::{Arc, LazyLock, Mutex}; use lru::LruCache; use pgt_query_ext::diagnostics::*; use pgt_text_size::TextRange; use pgt_tokenizer::tokenize; +use regex::Regex; use super::statement_identifier::StatementId; @@ -82,13 +83,28 @@ impl PgQueryStore { let range = TextRange::new(start.try_into().unwrap(), end.try_into().unwrap()); let r = pgt_query::parse_plpgsql(statement.content()) - .map_err(|err| SyntaxDiagnostic::new(err.to_string(), Some(range))); + .or_else(|e| match &e { + // ignore `is not a known variable` for composite types because libpg_query reports a false positive. + // https://github.com/pganalyze/libpg_query/issues/159 + pgt_query::Error::Parse(err) if is_composite_type_error(err) => Ok(()), + _ => Err(e), + }) + .map_err(|e| SyntaxDiagnostic::new(e.to_string(), Some(range))); + cache.put(statement.clone(), r.clone()); Some(r) } } +static COMPOSITE_TYPE_ERROR_RE: LazyLock = LazyLock::new(|| { + Regex::new(r#"Invalid statement: "([^"]+\.[^"]+)" is not a known variable"#).unwrap() +}); + +fn is_composite_type_error(err: &str) -> bool { + COMPOSITE_TYPE_ERROR_RE.is_match(err) +} + /// Converts named parameters in a SQL query string to positional parameters. /// /// This function scans the input SQL string for named parameters (e.g., `@param`, `:param`, `:'param'`) From 38e9ba889b369f1f320b1850ae9e7aa3573dbf24 Mon Sep 17 00:00:00 2001 From: psteinroe Date: Mon, 1 Sep 2025 10:59:54 +0200 Subject: [PATCH 2/2] progress --- crates/pgt_workspace/src/workspace/server/pg_query.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/crates/pgt_workspace/src/workspace/server/pg_query.rs b/crates/pgt_workspace/src/workspace/server/pg_query.rs index 839bd64c3..0494c5219 100644 --- a/crates/pgt_workspace/src/workspace/server/pg_query.rs +++ b/crates/pgt_workspace/src/workspace/server/pg_query.rs @@ -97,9 +97,8 @@ impl PgQueryStore { } } -static COMPOSITE_TYPE_ERROR_RE: LazyLock = LazyLock::new(|| { - Regex::new(r#"Invalid statement: "([^"]+\.[^"]+)" is not a known variable"#).unwrap() -}); +static COMPOSITE_TYPE_ERROR_RE: LazyLock = + LazyLock::new(|| Regex::new(r#"\\?"([^"\\]+\.[^"\\]+)\\?" is not a known variable"#).unwrap()); fn is_composite_type_error(err: &str) -> bool { COMPOSITE_TYPE_ERROR_RE.is_match(err)