From 33fc1d850499f1b76629492431b6f01563b5d55c Mon Sep 17 00:00:00 2001 From: psteinroe Date: Mon, 29 Dec 2025 13:40:53 +0100 Subject: [PATCH 01/16] feat(dblint): add splinter --- .github/actions/setup-postgres/action.yml | 37 +- .gitignore | 1 + Cargo.lock | 139 +-- Cargo.toml | 2 +- Dockerfile | 34 +- crates/pgls_configuration/src/lib.rs | 12 + crates/pgls_configuration/src/pglinter/mod.rs | 41 + .../pgls_configuration/src/pglinter/rules.rs | 916 ++++++++++++++++++ .../pgls_configuration/src/rules/selector.rs | 24 +- .../src/categories.rs | 36 + crates/pgls_pglinter/Cargo.toml | 29 + crates/pgls_pglinter/src/cache.rs | 58 ++ crates/pgls_pglinter/src/diagnostics.rs | 183 ++++ crates/pgls_pglinter/src/lib.rs | 157 +++ crates/pgls_pglinter/src/registry.rs | 463 +++++++++ crates/pgls_pglinter/src/rule.rs | 21 + .../composite_primary_key_too_many_columns.rs | 15 + .../base/how_many_objects_with_uppercase.rs | 12 + .../src/rules/base/how_many_redudant_index.rs | 13 + .../how_many_table_without_index_on_fk.rs | 12 + .../how_many_table_without_primary_key.rs | 12 + .../base/how_many_tables_never_selected.rs | 12 + .../base/how_many_tables_with_fk_mismatch.rs | 15 + .../how_many_tables_with_fk_outside_schema.rs | 15 + .../how_many_tables_with_reserved_keywords.rs | 15 + .../base/how_many_tables_with_same_trigger.rs | 15 + .../src/rules/base/how_many_unused_index.rs | 12 + crates/pgls_pglinter/src/rules/base/mod.rs | 16 + .../base/several_table_owner_in_schema.rs | 12 + crates/pgls_pglinter/src/rules/cluster/mod.rs | 7 + .../cluster/password_encryption_is_md5.rs | 15 + ...hod_trust_or_password_should_not_exists.rs | 11 + ...ies_with_method_trust_should_not_exists.rs | 11 + crates/pgls_pglinter/src/rules/mod.rs | 7 + crates/pgls_pglinter/src/rules/schema/mod.rs | 9 + .../schema/owner_schema_is_internal_role.rs | 11 + .../schema_owner_do_not_match_table_owner.rs | 12 + .../schema_prefixed_or_suffixed_with_envt.rs | 13 + .../schema_with_default_role_not_granted.rs | 14 + .../rules/schema/unsecured_public_schema.rs | 11 + crates/pgls_pglinter/src/sarif.rs | 172 ++++ crates/pgls_pglinter/tests/diagnostics.rs | 267 +++++ crates/pgls_workspace/src/settings.rs | 41 + docs/schema.json | 368 +++++++ justfile | 1 + .../backend-jsonrpc/src/workspace.ts | 174 +++- .../backend-jsonrpc/src/workspace.ts | 174 +++- xtask/codegen/Cargo.toml | 2 + xtask/codegen/src/generate_configuration.rs | 8 +- xtask/codegen/src/generate_pglinter.rs | 687 +++++++++++++ xtask/codegen/src/lib.rs | 5 + xtask/codegen/src/main.rs | 5 +- 52 files changed, 4216 insertions(+), 138 deletions(-) create mode 100644 crates/pgls_configuration/src/pglinter/mod.rs create mode 100644 crates/pgls_configuration/src/pglinter/rules.rs create mode 100644 crates/pgls_pglinter/Cargo.toml create mode 100644 crates/pgls_pglinter/src/cache.rs create mode 100644 crates/pgls_pglinter/src/diagnostics.rs create mode 100644 crates/pgls_pglinter/src/lib.rs create mode 100644 crates/pgls_pglinter/src/registry.rs create mode 100644 crates/pgls_pglinter/src/rule.rs create mode 100644 crates/pgls_pglinter/src/rules/base/composite_primary_key_too_many_columns.rs create mode 100644 crates/pgls_pglinter/src/rules/base/how_many_objects_with_uppercase.rs create mode 100644 crates/pgls_pglinter/src/rules/base/how_many_redudant_index.rs create mode 100644 crates/pgls_pglinter/src/rules/base/how_many_table_without_index_on_fk.rs create mode 100644 crates/pgls_pglinter/src/rules/base/how_many_table_without_primary_key.rs create mode 100644 crates/pgls_pglinter/src/rules/base/how_many_tables_never_selected.rs create mode 100644 crates/pgls_pglinter/src/rules/base/how_many_tables_with_fk_mismatch.rs create mode 100644 crates/pgls_pglinter/src/rules/base/how_many_tables_with_fk_outside_schema.rs create mode 100644 crates/pgls_pglinter/src/rules/base/how_many_tables_with_reserved_keywords.rs create mode 100644 crates/pgls_pglinter/src/rules/base/how_many_tables_with_same_trigger.rs create mode 100644 crates/pgls_pglinter/src/rules/base/how_many_unused_index.rs create mode 100644 crates/pgls_pglinter/src/rules/base/mod.rs create mode 100644 crates/pgls_pglinter/src/rules/base/several_table_owner_in_schema.rs create mode 100644 crates/pgls_pglinter/src/rules/cluster/mod.rs create mode 100644 crates/pgls_pglinter/src/rules/cluster/password_encryption_is_md5.rs create mode 100644 crates/pgls_pglinter/src/rules/cluster/pg_hba_entries_with_method_trust_or_password_should_not_exists.rs create mode 100644 crates/pgls_pglinter/src/rules/cluster/pg_hba_entries_with_method_trust_should_not_exists.rs create mode 100644 crates/pgls_pglinter/src/rules/mod.rs create mode 100644 crates/pgls_pglinter/src/rules/schema/mod.rs create mode 100644 crates/pgls_pglinter/src/rules/schema/owner_schema_is_internal_role.rs create mode 100644 crates/pgls_pglinter/src/rules/schema/schema_owner_do_not_match_table_owner.rs create mode 100644 crates/pgls_pglinter/src/rules/schema/schema_prefixed_or_suffixed_with_envt.rs create mode 100644 crates/pgls_pglinter/src/rules/schema/schema_with_default_role_not_granted.rs create mode 100644 crates/pgls_pglinter/src/rules/schema/unsecured_public_schema.rs create mode 100644 crates/pgls_pglinter/src/sarif.rs create mode 100644 crates/pgls_pglinter/tests/diagnostics.rs create mode 100644 xtask/codegen/src/generate_pglinter.rs diff --git a/.github/actions/setup-postgres/action.yml b/.github/actions/setup-postgres/action.yml index 5a37a9a27..ef54fde2b 100644 --- a/.github/actions/setup-postgres/action.yml +++ b/.github/actions/setup-postgres/action.yml @@ -59,20 +59,47 @@ runs: echo "Extension library files:" ls -la "$(pg_config --pkglibdir)/" | grep plpgsql || echo "No plpgsql_check library found" - # Install the pglpgsql_check extension on macOS (Part 2) - - name: Create extension in database + # Install the pglinter extension on macOS + - name: Install and compile pglinter + if: runner.os == 'macOS' + shell: bash + run: | + # First, ensure we're using the same PostgreSQL that the action installed + export PATH="$(pg_config --bindir):$PATH" + + # Clone and build pglinter + git clone https://github.com/pmpetit/pglinter.git + cd pglinter + + # Clean and compile + make USE_PGXS=1 clean + make USE_PGXS=1 all + + # Install (may need sudo depending on permissions) + sudo make USE_PGXS=1 install + + # Verify installation + echo "Extension control files:" + ls -la "$(pg_config --sharedir)/extension/" | grep pglinter || echo "No pglinter found" + + echo "Extension library files:" + ls -la "$(pg_config --pkglibdir)/" | grep pglinter || echo "No pglinter library found" + + # Create extensions in database on macOS + - name: Create extensions in database if: runner.os == 'macOS' shell: bash env: PGSERVICE: ${{ steps.postgres.outputs.service-name }} run: | psql -c "CREATE EXTENSION plpgsql_check;" + psql -c "CREATE EXTENSION pglinter;" # Verify installation - psql -c "SELECT extname, extversion FROM pg_extension WHERE extname = 'plpgsql_check';" + psql -c "SELECT extname, extversion FROM pg_extension WHERE extname IN ('plpgsql_check', 'pglinter');" - # For Linux, use custom Docker image with plpgsql_check - - name: Build and start PostgreSQL with plpgsql_check + # For Linux, use custom Docker image with plpgsql_check and pglinter + - name: Build and start PostgreSQL with extensions if: runner.os == 'Linux' shell: bash run: | diff --git a/.gitignore b/.gitignore index 341877378..af3907006 100644 --- a/.gitignore +++ b/.gitignore @@ -51,3 +51,4 @@ site/ biome-main/ .review/ +pglinter_repo/ diff --git a/Cargo.lock b/Cargo.lock index 4a60ff9ba..1fc0daaff 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -771,12 +771,6 @@ version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "325918d6fe32f23b19878fe4b34794ae41fc19ddbe53b10571a4874d44ffd39b" -[[package]] -name = "camino" -version = "1.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e629a66d692cb9ff1a1c664e41771b3dcaf961985a9774c0eb0bd1b51cf60a48" - [[package]] name = "cast" version = "0.3.0" @@ -1163,27 +1157,6 @@ dependencies = [ "subtle", ] -[[package]] -name = "dir-test" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62c013fe825864f3e4593f36426c1fa7a74f5603f13ca8d1af7a990c1cd94a79" -dependencies = [ - "dir-test-macros", -] - -[[package]] -name = "dir-test-macros" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d42f54d7b4a6bc2400fe5b338e35d1a335787585375322f49c5d5fe7b243da7e" -dependencies = [ - "glob", - "proc-macro2", - "quote", - "syn 2.0.90", -] - [[package]] name = "directories" version = "5.0.1" @@ -1436,15 +1409,6 @@ dependencies = [ "miniz_oxide", ] -[[package]] -name = "fluent-uri" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17c704e9dbe1ddd863da1e6ff3567795087b1eb201ce80d8fa81162e1516500d" -dependencies = [ - "bitflags 1.3.2", -] - [[package]] name = "flume" version = "0.11.1" @@ -2136,15 +2100,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "json-strip-comments" -version = "3.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25376d12b2f6ae53f986f86e2a808a56af03d72284ae24fc35a2e290d09ee3c3" -dependencies = [ - "memchr", -] - [[package]] name = "kv-log-macro" version = "1.0.7" @@ -2315,19 +2270,6 @@ dependencies = [ "url", ] -[[package]] -name = "lsp-types" -version = "0.97.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53353550a17c04ac46c585feb189c2db82154fc84b79c7a66c96c2c644f66071" -dependencies = [ - "bitflags 1.3.2", - "fluent-uri", - "serde", - "serde_json", - "serde_repr", -] - [[package]] name = "matchers" version = "0.1.0" @@ -2349,9 +2291,9 @@ dependencies = [ [[package]] name = "memchr" -version = "2.7.6" +version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" [[package]] name = "memoffset" @@ -2607,7 +2549,7 @@ dependencies = [ "dashmap 6.1.0", "dunce", "indexmap 2.7.0", - "json-strip-comments 1.0.4", + "json-strip-comments", "once_cell", "rustc-hash 2.1.0", "serde", @@ -2754,7 +2696,6 @@ dependencies = [ "pgls_env", "pgls_fs", "pgls_lsp", - "pgls_schema_cache", "pgls_test_utils", "pgls_text_edit", "pgls_workspace", @@ -2777,6 +2718,7 @@ dependencies = [ name = "pgls_completions" version = "0.0.0" dependencies = [ + "async-std", "criterion", "fuzzy-matcher", "insta", @@ -2790,6 +2732,7 @@ dependencies = [ "serde", "serde_json", "sqlx", + "tokio", "tracing", "tree-sitter", "unindent", @@ -2809,8 +2752,6 @@ dependencies = [ "pgls_console", "pgls_diagnostics", "pgls_env", - "pgls_matcher", - "pgls_pretty_print", "pgls_text_size", "rustc-hash 2.1.0", "schemars", @@ -2913,6 +2854,7 @@ dependencies = [ "serde", "serde_json", "sqlx", + "tokio", "tracing", "tree-sitter", ] @@ -2980,12 +2922,20 @@ dependencies = [ ] [[package]] -name = "pgls_matcher" +name = "pgls_pglinter" version = "0.0.0" dependencies = [ + "insta", + "pgls_analyse", "pgls_console", "pgls_diagnostics", + "pgls_diagnostics_categories", + "pgls_schema_cache", + "pgls_test_utils", "rustc-hash 2.1.0", + "serde", + "serde_json", + "sqlx", ] [[package]] @@ -3006,33 +2956,6 @@ dependencies = [ "tree-sitter", ] -[[package]] -name = "pgls_pretty_print" -version = "0.0.0" -dependencies = [ - "camino", - "dir-test", - "insta", - "pgls_pretty_print_codegen", - "pgls_query", - "pgls_statement_splitter", - "regex", - "thiserror 1.0.69", -] - -[[package]] -name = "pgls_pretty_print_codegen" -version = "0.0.0" -dependencies = [ - "anyhow", - "convert_case", - "proc-macro2", - "prost-reflect", - "protox", - "quote", - "ureq", -] - [[package]] name = "pgls_query" version = "0.0.0" @@ -3092,13 +3015,10 @@ dependencies = [ name = "pgls_splinter" version = "0.0.0" dependencies = [ - "biome_deserialize 0.6.0", "insta", "pgls_analyse", - "pgls_configuration", "pgls_console", "pgls_diagnostics", - "pgls_matcher", "pgls_schema_cache", "pgls_test_utils", "serde", @@ -3197,7 +3117,6 @@ name = "pgls_treesitter_grammar" version = "0.0.0" dependencies = [ "cc", - "criterion", "insta", "pgls_test_utils", "tree-sitter", @@ -3236,26 +3155,6 @@ dependencies = [ "uuid", ] -[[package]] -name = "pgls_wasm" -version = "0.0.0" -dependencies = [ - "lsp-types 0.97.0", - "pgls_analyse", - "pgls_completions", - "pgls_configuration", - "pgls_diagnostics", - "pgls_fs", - "pgls_query", - "pgls_schema_cache", - "pgls_text_size", - "pgls_treesitter_grammar", - "pgls_workspace", - "serde", - "serde_json", - "tree-sitter", -] - [[package]] name = "pgls_workspace" version = "0.0.0" @@ -3267,7 +3166,6 @@ dependencies = [ "futures", "globset", "ignore", - "json-strip-comments 3.1.0", "lru", "pgls_analyse", "pgls_analyser", @@ -3279,9 +3177,7 @@ dependencies = [ "pgls_fs", "pgls_hover", "pgls_lexer", - "pgls_matcher", "pgls_plpgsql_check", - "pgls_pretty_print", "pgls_query", "pgls_query_ext", "pgls_schema_cache", @@ -3289,7 +3185,6 @@ dependencies = [ "pgls_statement_splitter", "pgls_suppressions", "pgls_test_utils", - "pgls_text_edit", "pgls_text_size", "pgls_tokenizer", "pgls_treesitter_grammar", @@ -4915,7 +4810,7 @@ dependencies = [ "dashmap 5.5.3", "futures", "httparse", - "lsp-types 0.94.1", + "lsp-types", "memchr", "serde", "serde_json", @@ -5742,11 +5637,13 @@ dependencies = [ "pgls_analyser", "pgls_diagnostics", "pgls_env", + "pgls_pglinter", "pgls_splinter", "pgls_workspace", "proc-macro2", "pulldown-cmark", "quote", + "regex", "xtask", ] diff --git a/Cargo.toml b/Cargo.toml index 16df96e3e..bdfbe4887 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -80,7 +80,7 @@ pgls_lexer = { path = "./crates/pgls_lexer", version = "0.0.0" pgls_lexer_codegen = { path = "./crates/pgls_lexer_codegen", version = "0.0.0" } pgls_lsp = { path = "./crates/pgls_lsp", version = "0.0.0" } pgls_markup = { path = "./crates/pgls_markup", version = "0.0.0" } -pgls_matcher = { path = "./crates/pgls_matcher", version = "0.0.0" } +pgls_pglinter = { path = "./crates/pgls_pglinter", version = "0.0.0" } pgls_plpgsql_check = { path = "./crates/pgls_plpgsql_check", version = "0.0.0" } pgls_pretty_print = { path = "./crates/pgls_pretty_print", version = "0.0.0" } pgls_pretty_print_codegen = { path = "./crates/pgls_pretty_print_codegen", version = "0.0.0" } diff --git a/Dockerfile b/Dockerfile index df374bce2..a26c74eb3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,15 +2,39 @@ FROM postgres:15 # Install build dependencies RUN apt-get update && \ - apt-get install -y postgresql-server-dev-15 gcc make git libicu-dev && \ + apt-get install -y postgresql-server-dev-15 gcc make git curl pkg-config libssl-dev libclang-dev clang libicu-dev && \ + # Install plpgsql_check (C extension - simple make install) + # Pin to v2.7.11 for stability with PG15 cd /tmp && \ git clone --branch v2.7.11 --depth 1 https://github.com/okbob/plpgsql_check.git && \ cd plpgsql_check && \ make && \ make install && \ - apt-get remove -y postgresql-server-dev-15 gcc make git libicu-dev && \ + cd /tmp && \ + rm -rf /tmp/plpgsql_check && \ + # Install Rust for pglinter (pgrx-based extension) + curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y && \ + . $HOME/.cargo/env && \ + # Install cargo-pgrx (version must match pglinter's pgrx dependency) + cargo install cargo-pgrx --version 0.16.1 --locked && \ + # Initialize pgrx for PostgreSQL 15 + cargo pgrx init --pg15 $(which pg_config) && \ + # Clone and build pglinter (using feat/83/violation_list branch for get_violations API + rule_messages) + cd /tmp && \ + git clone -b feat/83/violation_list https://github.com/pmpetit/pglinter.git && \ + cd pglinter && \ + cargo pgrx install --pg-config $(which pg_config) --release && \ + # Cleanup Rust and build dependencies + rm -rf /tmp/pglinter $HOME/.cargo $HOME/.rustup && \ + apt-get remove -y gcc make git curl pkg-config libssl-dev libclang-dev clang libicu-dev && \ apt-get autoremove -y && \ - rm -rf /tmp/plpgsql_check /var/lib/apt/lists/* + rm -rf /var/lib/apt/lists/* -# Add initialization script directly -RUN echo "CREATE EXTENSION IF NOT EXISTS plpgsql_check;" > /docker-entrypoint-initdb.d/01-create-extension.sql \ No newline at end of file +# Add initialization script for extensions +# Only create in postgres database (NOT template1) to avoid polluting test databases +# Tests that need extensions can create them explicitly +RUN printf '%s\n' \ + "CREATE SCHEMA IF NOT EXISTS extensions;" \ + "CREATE EXTENSION IF NOT EXISTS plpgsql_check SCHEMA extensions;" \ + "CREATE EXTENSION IF NOT EXISTS pglinter SCHEMA extensions;" \ + > /docker-entrypoint-initdb.d/01-create-extension.sql diff --git a/crates/pgls_configuration/src/lib.rs b/crates/pgls_configuration/src/lib.rs index 86565902c..324e394b3 100644 --- a/crates/pgls_configuration/src/lib.rs +++ b/crates/pgls_configuration/src/lib.rs @@ -8,6 +8,7 @@ pub mod files; pub mod format; pub mod linter; pub mod migrations; +pub mod pglinter; pub mod plpgsql_check; pub mod rules; pub mod splinter; @@ -37,6 +38,9 @@ pub use linter::{ use migrations::{ MigrationsConfiguration, PartialMigrationsConfiguration, partial_migrations_configuration, }; +use pglinter::{ + PartialPglinterConfiguration, PglinterConfiguration, partial_pglinter_configuration, +}; use pgls_env::PGLS_WEBSITE; use plpgsql_check::{ PartialPlPgSqlCheckConfiguration, PlPgSqlCheckConfiguration, @@ -102,6 +106,10 @@ pub struct Configuration { #[partial(type, bpaf(external(partial_format_configuration), optional))] pub format: FormatConfiguration, + /// The configuration for pglinter + #[partial(type, bpaf(external(partial_pglinter_configuration), optional))] + pub pglinter: PglinterConfiguration, + /// The configuration for type checking #[partial(type, bpaf(external(partial_typecheck_configuration), optional))] pub typecheck: TypecheckConfiguration, @@ -151,6 +159,10 @@ impl PartialConfiguration { enabled: Some(false), // Disabled by default during beta ..Default::default() }), + pglinter: Some(PartialPglinterConfiguration { + enabled: Some(false), // Disabled by default since pglinter extension might not be installed + ..Default::default() + }), typecheck: Some(PartialTypecheckConfiguration { enabled: Some(true), ..Default::default() diff --git a/crates/pgls_configuration/src/pglinter/mod.rs b/crates/pgls_configuration/src/pglinter/mod.rs new file mode 100644 index 000000000..f676abf1f --- /dev/null +++ b/crates/pgls_configuration/src/pglinter/mod.rs @@ -0,0 +1,41 @@ +//! Generated file, do not edit by hand, see `xtask/codegen` + +#![doc = r" Generated file, do not edit by hand, see `xtask/codegen`"] +mod rules; +use biome_deserialize_macros::{Merge, Partial}; +use bpaf::Bpaf; +pub use rules::*; +use serde::{Deserialize, Serialize}; +#[derive(Clone, Debug, Deserialize, Eq, Partial, PartialEq, Serialize)] +#[partial(derive(Bpaf, Clone, Eq, Merge, PartialEq))] +#[partial(cfg_attr(feature = "schema", derive(schemars::JsonSchema)))] +#[partial(serde(rename_all = "camelCase", default, deny_unknown_fields))] +pub struct PglinterConfiguration { + #[doc = r" if `false`, it disables the feature and the linter won't be executed. `true` by default"] + #[partial(bpaf(hide))] + pub enabled: bool, + #[doc = r" List of rules"] + #[partial(bpaf(pure(Default::default()), optional, hide))] + pub rules: Rules, +} +impl PglinterConfiguration { + pub const fn is_disabled(&self) -> bool { + !self.enabled + } +} +impl Default for PglinterConfiguration { + fn default() -> Self { + Self { + enabled: true, + rules: Default::default(), + } + } +} +impl PartialPglinterConfiguration { + pub const fn is_disabled(&self) -> bool { + matches!(self.enabled, Some(false)) + } + pub fn get_rules(&self) -> Rules { + self.rules.clone().unwrap_or_default() + } +} diff --git a/crates/pgls_configuration/src/pglinter/rules.rs b/crates/pgls_configuration/src/pglinter/rules.rs new file mode 100644 index 000000000..b22721a80 --- /dev/null +++ b/crates/pgls_configuration/src/pglinter/rules.rs @@ -0,0 +1,916 @@ +//! Generated file, do not edit by hand, see `xtask/codegen` + +#![doc = r" Generated file, do not edit by hand, see `xtask/codegen`"] +use crate::rules::{RuleConfiguration, RulePlainConfiguration}; +use biome_deserialize_macros::Merge; +use pgls_analyse::RuleFilter; +use pgls_analyser::RuleOptions; +use pgls_diagnostics::{Category, Severity}; +use rustc_hash::FxHashSet; +#[cfg(feature = "schema")] +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +#[derive( + Clone, + Copy, + Debug, + Eq, + Hash, + Merge, + Ord, + PartialEq, + PartialOrd, + serde :: Deserialize, + serde :: Serialize, +)] +#[cfg_attr(feature = "schema", derive(JsonSchema))] +#[serde(rename_all = "camelCase")] +pub enum RuleGroup { + Base, + Cluster, + Schema, +} +impl RuleGroup { + pub const fn as_str(self) -> &'static str { + match self { + Self::Base => Base::GROUP_NAME, + Self::Cluster => Cluster::GROUP_NAME, + Self::Schema => Schema::GROUP_NAME, + } + } +} +impl std::str::FromStr for RuleGroup { + type Err = &'static str; + fn from_str(s: &str) -> Result { + match s { + Base::GROUP_NAME => Ok(Self::Base), + Cluster::GROUP_NAME => Ok(Self::Cluster), + Schema::GROUP_NAME => Ok(Self::Schema), + _ => Err("This rule group doesn't exist."), + } + } +} +#[derive(Clone, Debug, Default, Deserialize, Eq, Merge, PartialEq, Serialize)] +#[cfg_attr(feature = "schema", derive(JsonSchema))] +#[cfg_attr(feature = "schema", schemars(rename = "PglinterRules"))] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct Rules { + #[doc = r" It enables the lint rules recommended by Postgres Language Server. `true` by default."] + #[serde(skip_serializing_if = "Option::is_none")] + pub recommended: Option, + #[doc = r" It enables ALL rules. The rules that belong to `nursery` won't be enabled."] + #[serde(skip_serializing_if = "Option::is_none")] + pub all: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub base: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub cluster: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub schema: Option, +} +impl Rules { + #[doc = r" Checks if the code coming from [pgls_diagnostics::Diagnostic] corresponds to a rule."] + #[doc = r" Usually the code is built like {group}/{rule_name}"] + pub fn has_rule(group: RuleGroup, rule_name: &str) -> Option<&'static str> { + match group { + RuleGroup::Base => Base::has_rule(rule_name), + RuleGroup::Cluster => Cluster::has_rule(rule_name), + RuleGroup::Schema => Schema::has_rule(rule_name), + } + } + #[doc = r" Given a category coming from [Diagnostic](pgls_diagnostics::Diagnostic), this function returns"] + #[doc = r" the [Severity](pgls_diagnostics::Severity) associated to the rule, if the configuration changed it."] + #[doc = r" If the severity is off or not set, then the function returns the default severity of the rule,"] + #[doc = r" which is configured at the rule definition."] + #[doc = r" The function can return `None` if the rule is not properly configured."] + pub fn get_severity_from_code(&self, category: &Category) -> Option { + let mut split_code = category.name().split('/'); + let _category_prefix = split_code.next(); + debug_assert_eq!(_category_prefix, Some("pglinter")); + let group = ::from_str(split_code.next()?).ok()?; + let rule_name = split_code.next()?; + let rule_name = Self::has_rule(group, rule_name)?; + let severity = match group { + RuleGroup::Base => self + .base + .as_ref() + .and_then(|group| group.get_rule_configuration(rule_name)) + .filter(|(level, _)| !matches!(level, RulePlainConfiguration::Off)) + .map_or_else(|| Base::severity(rule_name), |(level, _)| level.into()), + RuleGroup::Cluster => self + .cluster + .as_ref() + .and_then(|group| group.get_rule_configuration(rule_name)) + .filter(|(level, _)| !matches!(level, RulePlainConfiguration::Off)) + .map_or_else(|| Cluster::severity(rule_name), |(level, _)| level.into()), + RuleGroup::Schema => self + .schema + .as_ref() + .and_then(|group| group.get_rule_configuration(rule_name)) + .filter(|(level, _)| !matches!(level, RulePlainConfiguration::Off)) + .map_or_else(|| Schema::severity(rule_name), |(level, _)| level.into()), + }; + Some(severity) + } + #[doc = r" Ensure that `recommended` is set to `true` or implied."] + pub fn set_recommended(&mut self) { + if self.all != Some(true) && self.recommended == Some(false) { + self.recommended = Some(true) + } + if let Some(group) = &mut self.base { + group.recommended = None; + } + if let Some(group) = &mut self.cluster { + group.recommended = None; + } + if let Some(group) = &mut self.schema { + group.recommended = None; + } + } + pub(crate) const fn is_recommended_false(&self) -> bool { + matches!(self.recommended, Some(false)) + } + pub(crate) const fn is_all_true(&self) -> bool { + matches!(self.all, Some(true)) + } + #[doc = r" It returns the enabled rules by default."] + #[doc = r""] + #[doc = r" The enabled rules are calculated from the difference with the disabled rules."] + pub fn as_enabled_rules(&self) -> FxHashSet> { + let mut enabled_rules = FxHashSet::default(); + let mut disabled_rules = FxHashSet::default(); + if let Some(group) = self.base.as_ref() { + group.collect_preset_rules( + self.is_all_true(), + !self.is_recommended_false(), + &mut enabled_rules, + ); + enabled_rules.extend(&group.get_enabled_rules()); + disabled_rules.extend(&group.get_disabled_rules()); + } else if self.is_all_true() { + enabled_rules.extend(Base::all_rules_as_filters()); + } else if !self.is_recommended_false() { + enabled_rules.extend(Base::recommended_rules_as_filters()); + } + if let Some(group) = self.cluster.as_ref() { + group.collect_preset_rules( + self.is_all_true(), + !self.is_recommended_false(), + &mut enabled_rules, + ); + enabled_rules.extend(&group.get_enabled_rules()); + disabled_rules.extend(&group.get_disabled_rules()); + } else if self.is_all_true() { + enabled_rules.extend(Cluster::all_rules_as_filters()); + } else if !self.is_recommended_false() { + enabled_rules.extend(Cluster::recommended_rules_as_filters()); + } + if let Some(group) = self.schema.as_ref() { + group.collect_preset_rules( + self.is_all_true(), + !self.is_recommended_false(), + &mut enabled_rules, + ); + enabled_rules.extend(&group.get_enabled_rules()); + disabled_rules.extend(&group.get_disabled_rules()); + } else if self.is_all_true() { + enabled_rules.extend(Schema::all_rules_as_filters()); + } else if !self.is_recommended_false() { + enabled_rules.extend(Schema::recommended_rules_as_filters()); + } + enabled_rules.difference(&disabled_rules).copied().collect() + } + #[doc = r" It returns the disabled rules by configuration."] + pub fn as_disabled_rules(&self) -> FxHashSet> { + let mut disabled_rules = FxHashSet::default(); + if let Some(group) = self.base.as_ref() { + disabled_rules.extend(&group.get_disabled_rules()); + } + if let Some(group) = self.cluster.as_ref() { + disabled_rules.extend(&group.get_disabled_rules()); + } + if let Some(group) = self.schema.as_ref() { + disabled_rules.extend(&group.get_disabled_rules()); + } + disabled_rules + } +} +#[derive(Clone, Debug, Default, Deserialize, Eq, Merge, PartialEq, Serialize)] +#[cfg_attr(feature = "schema", derive(JsonSchema))] +#[serde(rename_all = "camelCase", default, deny_unknown_fields)] +#[doc = r" A list of rules that belong to this group"] +pub struct Base { + #[doc = r" It enables the recommended rules for this group"] + #[serde(skip_serializing_if = "Option::is_none")] + pub recommended: Option, + #[doc = r" It enables ALL rules for this group."] + #[serde(skip_serializing_if = "Option::is_none")] + pub all: Option, + #[doc = "CompositePrimaryKeyTooManyColumns (B012): Detect tables with composite primary keys involving more than 4 columns"] + #[serde(skip_serializing_if = "Option::is_none")] + pub composite_primary_key_too_many_columns: Option>, + #[doc = "HowManyObjectsWithUppercase (B005): Count number of objects with uppercase in name or in columns."] + #[serde(skip_serializing_if = "Option::is_none")] + pub how_many_objects_with_uppercase: Option>, + #[doc = "HowManyRedudantIndex (B002): Count number of redundant index vs nb index."] + #[serde(skip_serializing_if = "Option::is_none")] + pub how_many_redudant_index: Option>, + #[doc = "HowManyTableWithoutIndexOnFk (B003): Count number of tables without index on foreign key."] + #[serde(skip_serializing_if = "Option::is_none")] + pub how_many_table_without_index_on_fk: Option>, + #[doc = "HowManyTableWithoutPrimaryKey (B001): Count number of tables without primary key."] + #[serde(skip_serializing_if = "Option::is_none")] + pub how_many_table_without_primary_key: Option>, + #[doc = "HowManyTablesNeverSelected (B006): Count number of table(s) that has never been selected."] + #[serde(skip_serializing_if = "Option::is_none")] + pub how_many_tables_never_selected: Option>, + #[doc = "HowManyTablesWithFkMismatch (B008): Count number of tables with foreign keys that do not match the key reference type."] + #[serde(skip_serializing_if = "Option::is_none")] + pub how_many_tables_with_fk_mismatch: Option>, + #[doc = "HowManyTablesWithFkOutsideSchema (B007): Count number of tables with foreign keys outside their schema."] + #[serde(skip_serializing_if = "Option::is_none")] + pub how_many_tables_with_fk_outside_schema: Option>, + #[doc = "HowManyTablesWithReservedKeywords (B010): Count number of database objects using reserved keywords in their names."] + #[serde(skip_serializing_if = "Option::is_none")] + pub how_many_tables_with_reserved_keywords: Option>, + #[doc = "HowManyTablesWithSameTrigger (B009): Count number of tables using the same trigger vs nb table with their own triggers."] + #[serde(skip_serializing_if = "Option::is_none")] + pub how_many_tables_with_same_trigger: Option>, + #[doc = "HowManyUnusedIndex (B004): Count number of unused index vs nb index (base on pg_stat_user_indexes, indexes associated to unique constraints are discard.)"] + #[serde(skip_serializing_if = "Option::is_none")] + pub how_many_unused_index: Option>, + #[doc = "SeveralTableOwnerInSchema (B011): In a schema there are several tables owned by different owners."] + #[serde(skip_serializing_if = "Option::is_none")] + pub several_table_owner_in_schema: Option>, +} +impl Base { + const GROUP_NAME: &'static str = "base"; + pub(crate) const GROUP_RULES: &'static [&'static str] = &[ + "compositePrimaryKeyTooManyColumns", + "howManyObjectsWithUppercase", + "howManyRedudantIndex", + "howManyTableWithoutIndexOnFk", + "howManyTableWithoutPrimaryKey", + "howManyTablesNeverSelected", + "howManyTablesWithFkMismatch", + "howManyTablesWithFkOutsideSchema", + "howManyTablesWithReservedKeywords", + "howManyTablesWithSameTrigger", + "howManyUnusedIndex", + "severalTableOwnerInSchema", + ]; + const RECOMMENDED_RULES_AS_FILTERS: &'static [RuleFilter<'static>] = &[ + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[0]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[1]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[2]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[3]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[4]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[5]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[6]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[7]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[8]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[9]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[10]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[11]), + ]; + const ALL_RULES_AS_FILTERS: &'static [RuleFilter<'static>] = &[ + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[0]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[1]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[2]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[3]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[4]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[5]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[6]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[7]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[8]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[9]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[10]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[11]), + ]; + #[doc = r" Retrieves the recommended rules"] + pub(crate) fn is_recommended_true(&self) -> bool { + matches!(self.recommended, Some(true)) + } + pub(crate) fn is_recommended_unset(&self) -> bool { + self.recommended.is_none() + } + pub(crate) fn is_all_true(&self) -> bool { + matches!(self.all, Some(true)) + } + pub(crate) fn is_all_unset(&self) -> bool { + self.all.is_none() + } + pub(crate) fn get_enabled_rules(&self) -> FxHashSet> { + let mut index_set = FxHashSet::default(); + if let Some(rule) = self.composite_primary_key_too_many_columns.as_ref() { + if rule.is_enabled() { + index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[0])); + } + } + if let Some(rule) = self.how_many_objects_with_uppercase.as_ref() { + if rule.is_enabled() { + index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[1])); + } + } + if let Some(rule) = self.how_many_redudant_index.as_ref() { + if rule.is_enabled() { + index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[2])); + } + } + if let Some(rule) = self.how_many_table_without_index_on_fk.as_ref() { + if rule.is_enabled() { + index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[3])); + } + } + if let Some(rule) = self.how_many_table_without_primary_key.as_ref() { + if rule.is_enabled() { + index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[4])); + } + } + if let Some(rule) = self.how_many_tables_never_selected.as_ref() { + if rule.is_enabled() { + index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[5])); + } + } + if let Some(rule) = self.how_many_tables_with_fk_mismatch.as_ref() { + if rule.is_enabled() { + index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[6])); + } + } + if let Some(rule) = self.how_many_tables_with_fk_outside_schema.as_ref() { + if rule.is_enabled() { + index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[7])); + } + } + if let Some(rule) = self.how_many_tables_with_reserved_keywords.as_ref() { + if rule.is_enabled() { + index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[8])); + } + } + if let Some(rule) = self.how_many_tables_with_same_trigger.as_ref() { + if rule.is_enabled() { + index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[9])); + } + } + if let Some(rule) = self.how_many_unused_index.as_ref() { + if rule.is_enabled() { + index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[10])); + } + } + if let Some(rule) = self.several_table_owner_in_schema.as_ref() { + if rule.is_enabled() { + index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[11])); + } + } + index_set + } + pub(crate) fn get_disabled_rules(&self) -> FxHashSet> { + let mut index_set = FxHashSet::default(); + if let Some(rule) = self.composite_primary_key_too_many_columns.as_ref() { + if rule.is_disabled() { + index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[0])); + } + } + if let Some(rule) = self.how_many_objects_with_uppercase.as_ref() { + if rule.is_disabled() { + index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[1])); + } + } + if let Some(rule) = self.how_many_redudant_index.as_ref() { + if rule.is_disabled() { + index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[2])); + } + } + if let Some(rule) = self.how_many_table_without_index_on_fk.as_ref() { + if rule.is_disabled() { + index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[3])); + } + } + if let Some(rule) = self.how_many_table_without_primary_key.as_ref() { + if rule.is_disabled() { + index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[4])); + } + } + if let Some(rule) = self.how_many_tables_never_selected.as_ref() { + if rule.is_disabled() { + index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[5])); + } + } + if let Some(rule) = self.how_many_tables_with_fk_mismatch.as_ref() { + if rule.is_disabled() { + index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[6])); + } + } + if let Some(rule) = self.how_many_tables_with_fk_outside_schema.as_ref() { + if rule.is_disabled() { + index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[7])); + } + } + if let Some(rule) = self.how_many_tables_with_reserved_keywords.as_ref() { + if rule.is_disabled() { + index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[8])); + } + } + if let Some(rule) = self.how_many_tables_with_same_trigger.as_ref() { + if rule.is_disabled() { + index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[9])); + } + } + if let Some(rule) = self.how_many_unused_index.as_ref() { + if rule.is_disabled() { + index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[10])); + } + } + if let Some(rule) = self.several_table_owner_in_schema.as_ref() { + if rule.is_disabled() { + index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[11])); + } + } + index_set + } + #[doc = r" Checks if, given a rule name, matches one of the rules contained in this category"] + pub(crate) fn has_rule(rule_name: &str) -> Option<&'static str> { + Some(Self::GROUP_RULES[Self::GROUP_RULES.binary_search(&rule_name).ok()?]) + } + pub(crate) fn recommended_rules_as_filters() -> &'static [RuleFilter<'static>] { + Self::RECOMMENDED_RULES_AS_FILTERS + } + pub(crate) fn all_rules_as_filters() -> &'static [RuleFilter<'static>] { + Self::ALL_RULES_AS_FILTERS + } + #[doc = r" Select preset rules"] + pub(crate) fn collect_preset_rules( + &self, + parent_is_all: bool, + parent_is_recommended: bool, + enabled_rules: &mut FxHashSet>, + ) { + if self.is_all_true() || self.is_all_unset() && parent_is_all { + enabled_rules.extend(Self::all_rules_as_filters()); + } else if self.is_recommended_true() + || self.is_recommended_unset() && self.is_all_unset() && parent_is_recommended + { + enabled_rules.extend(Self::recommended_rules_as_filters()); + } + } + pub(crate) fn severity(rule_name: &str) -> Severity { + match rule_name { + "compositePrimaryKeyTooManyColumns" => Severity::Warning, + "howManyObjectsWithUppercase" => Severity::Warning, + "howManyRedudantIndex" => Severity::Warning, + "howManyTableWithoutIndexOnFk" => Severity::Warning, + "howManyTableWithoutPrimaryKey" => Severity::Warning, + "howManyTablesNeverSelected" => Severity::Warning, + "howManyTablesWithFkMismatch" => Severity::Warning, + "howManyTablesWithFkOutsideSchema" => Severity::Warning, + "howManyTablesWithReservedKeywords" => Severity::Warning, + "howManyTablesWithSameTrigger" => Severity::Warning, + "howManyUnusedIndex" => Severity::Warning, + "severalTableOwnerInSchema" => Severity::Warning, + _ => unreachable!(), + } + } + pub(crate) fn get_rule_configuration( + &self, + rule_name: &str, + ) -> Option<(RulePlainConfiguration, Option)> { + match rule_name { + "compositePrimaryKeyTooManyColumns" => self + .composite_primary_key_too_many_columns + .as_ref() + .map(|conf| (conf.level(), conf.get_options())), + "howManyObjectsWithUppercase" => self + .how_many_objects_with_uppercase + .as_ref() + .map(|conf| (conf.level(), conf.get_options())), + "howManyRedudantIndex" => self + .how_many_redudant_index + .as_ref() + .map(|conf| (conf.level(), conf.get_options())), + "howManyTableWithoutIndexOnFk" => self + .how_many_table_without_index_on_fk + .as_ref() + .map(|conf| (conf.level(), conf.get_options())), + "howManyTableWithoutPrimaryKey" => self + .how_many_table_without_primary_key + .as_ref() + .map(|conf| (conf.level(), conf.get_options())), + "howManyTablesNeverSelected" => self + .how_many_tables_never_selected + .as_ref() + .map(|conf| (conf.level(), conf.get_options())), + "howManyTablesWithFkMismatch" => self + .how_many_tables_with_fk_mismatch + .as_ref() + .map(|conf| (conf.level(), conf.get_options())), + "howManyTablesWithFkOutsideSchema" => self + .how_many_tables_with_fk_outside_schema + .as_ref() + .map(|conf| (conf.level(), conf.get_options())), + "howManyTablesWithReservedKeywords" => self + .how_many_tables_with_reserved_keywords + .as_ref() + .map(|conf| (conf.level(), conf.get_options())), + "howManyTablesWithSameTrigger" => self + .how_many_tables_with_same_trigger + .as_ref() + .map(|conf| (conf.level(), conf.get_options())), + "howManyUnusedIndex" => self + .how_many_unused_index + .as_ref() + .map(|conf| (conf.level(), conf.get_options())), + "severalTableOwnerInSchema" => self + .several_table_owner_in_schema + .as_ref() + .map(|conf| (conf.level(), conf.get_options())), + _ => None, + } + } +} +#[derive(Clone, Debug, Default, Deserialize, Eq, Merge, PartialEq, Serialize)] +#[cfg_attr(feature = "schema", derive(JsonSchema))] +#[serde(rename_all = "camelCase", default, deny_unknown_fields)] +#[doc = r" A list of rules that belong to this group"] +pub struct Cluster { + #[doc = r" It enables the recommended rules for this group"] + #[serde(skip_serializing_if = "Option::is_none")] + pub recommended: Option, + #[doc = r" It enables ALL rules for this group."] + #[serde(skip_serializing_if = "Option::is_none")] + pub all: Option, + #[doc = "PasswordEncryptionIsMd5 (C003): This configuration is not secure anymore and will prevent an upgrade to Postgres 18. Warning, you will need to reset all passwords after this is changed to scram-sha-256."] + #[serde(skip_serializing_if = "Option::is_none")] + pub password_encryption_is_md5: Option>, + #[doc = "PgHbaEntriesWithMethodTrustOrPasswordShouldNotExists (C002): This configuration is extremely insecure and should only be used in a controlled, non-production environment for testing purposes. In a production environment, you should use more secure authentication methods such as md5, scram-sha-256, or cert, and restrict access to trusted IP addresses only."] + #[serde(skip_serializing_if = "Option::is_none")] + pub pg_hba_entries_with_method_trust_or_password_should_not_exists: + Option>, + #[doc = "PgHbaEntriesWithMethodTrustShouldNotExists (C001): This configuration is extremely insecure and should only be used in a controlled, non-production environment for testing purposes. In a production environment, you should use more secure authentication methods such as md5, scram-sha-256, or cert, and restrict access to trusted IP addresses only."] + #[serde(skip_serializing_if = "Option::is_none")] + pub pg_hba_entries_with_method_trust_should_not_exists: Option>, +} +impl Cluster { + const GROUP_NAME: &'static str = "cluster"; + pub(crate) const GROUP_RULES: &'static [&'static str] = &[ + "passwordEncryptionIsMd5", + "pgHbaEntriesWithMethodTrustOrPasswordShouldNotExists", + "pgHbaEntriesWithMethodTrustShouldNotExists", + ]; + const RECOMMENDED_RULES_AS_FILTERS: &'static [RuleFilter<'static>] = &[ + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[0]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[1]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[2]), + ]; + const ALL_RULES_AS_FILTERS: &'static [RuleFilter<'static>] = &[ + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[0]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[1]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[2]), + ]; + #[doc = r" Retrieves the recommended rules"] + pub(crate) fn is_recommended_true(&self) -> bool { + matches!(self.recommended, Some(true)) + } + pub(crate) fn is_recommended_unset(&self) -> bool { + self.recommended.is_none() + } + pub(crate) fn is_all_true(&self) -> bool { + matches!(self.all, Some(true)) + } + pub(crate) fn is_all_unset(&self) -> bool { + self.all.is_none() + } + pub(crate) fn get_enabled_rules(&self) -> FxHashSet> { + let mut index_set = FxHashSet::default(); + if let Some(rule) = self.password_encryption_is_md5.as_ref() { + if rule.is_enabled() { + index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[0])); + } + } + if let Some(rule) = self + .pg_hba_entries_with_method_trust_or_password_should_not_exists + .as_ref() + { + if rule.is_enabled() { + index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[1])); + } + } + if let Some(rule) = self + .pg_hba_entries_with_method_trust_should_not_exists + .as_ref() + { + if rule.is_enabled() { + index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[2])); + } + } + index_set + } + pub(crate) fn get_disabled_rules(&self) -> FxHashSet> { + let mut index_set = FxHashSet::default(); + if let Some(rule) = self.password_encryption_is_md5.as_ref() { + if rule.is_disabled() { + index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[0])); + } + } + if let Some(rule) = self + .pg_hba_entries_with_method_trust_or_password_should_not_exists + .as_ref() + { + if rule.is_disabled() { + index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[1])); + } + } + if let Some(rule) = self + .pg_hba_entries_with_method_trust_should_not_exists + .as_ref() + { + if rule.is_disabled() { + index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[2])); + } + } + index_set + } + #[doc = r" Checks if, given a rule name, matches one of the rules contained in this category"] + pub(crate) fn has_rule(rule_name: &str) -> Option<&'static str> { + Some(Self::GROUP_RULES[Self::GROUP_RULES.binary_search(&rule_name).ok()?]) + } + pub(crate) fn recommended_rules_as_filters() -> &'static [RuleFilter<'static>] { + Self::RECOMMENDED_RULES_AS_FILTERS + } + pub(crate) fn all_rules_as_filters() -> &'static [RuleFilter<'static>] { + Self::ALL_RULES_AS_FILTERS + } + #[doc = r" Select preset rules"] + pub(crate) fn collect_preset_rules( + &self, + parent_is_all: bool, + parent_is_recommended: bool, + enabled_rules: &mut FxHashSet>, + ) { + if self.is_all_true() || self.is_all_unset() && parent_is_all { + enabled_rules.extend(Self::all_rules_as_filters()); + } else if self.is_recommended_true() + || self.is_recommended_unset() && self.is_all_unset() && parent_is_recommended + { + enabled_rules.extend(Self::recommended_rules_as_filters()); + } + } + pub(crate) fn severity(rule_name: &str) -> Severity { + match rule_name { + "passwordEncryptionIsMd5" => Severity::Warning, + "pgHbaEntriesWithMethodTrustOrPasswordShouldNotExists" => Severity::Warning, + "pgHbaEntriesWithMethodTrustShouldNotExists" => Severity::Warning, + _ => unreachable!(), + } + } + pub(crate) fn get_rule_configuration( + &self, + rule_name: &str, + ) -> Option<(RulePlainConfiguration, Option)> { + match rule_name { + "passwordEncryptionIsMd5" => self + .password_encryption_is_md5 + .as_ref() + .map(|conf| (conf.level(), conf.get_options())), + "pgHbaEntriesWithMethodTrustOrPasswordShouldNotExists" => self + .pg_hba_entries_with_method_trust_or_password_should_not_exists + .as_ref() + .map(|conf| (conf.level(), conf.get_options())), + "pgHbaEntriesWithMethodTrustShouldNotExists" => self + .pg_hba_entries_with_method_trust_should_not_exists + .as_ref() + .map(|conf| (conf.level(), conf.get_options())), + _ => None, + } + } +} +#[derive(Clone, Debug, Default, Deserialize, Eq, Merge, PartialEq, Serialize)] +#[cfg_attr(feature = "schema", derive(JsonSchema))] +#[serde(rename_all = "camelCase", default, deny_unknown_fields)] +#[doc = r" A list of rules that belong to this group"] +pub struct Schema { + #[doc = r" It enables the recommended rules for this group"] + #[serde(skip_serializing_if = "Option::is_none")] + pub recommended: Option, + #[doc = r" It enables ALL rules for this group."] + #[serde(skip_serializing_if = "Option::is_none")] + pub all: Option, + #[doc = "OwnerSchemaIsInternalRole (S004): Owner of schema should not be any internal pg roles, or owner is a superuser (not sure it is necesary)."] + #[serde(skip_serializing_if = "Option::is_none")] + pub owner_schema_is_internal_role: Option>, + #[doc = "SchemaOwnerDoNotMatchTableOwner (S005): The schema owner and tables in the schema do not match."] + #[serde(skip_serializing_if = "Option::is_none")] + pub schema_owner_do_not_match_table_owner: Option>, + #[doc = "SchemaPrefixedOrSuffixedWithEnvt (S002): The schema is prefixed with one of staging,stg,preprod,prod,sandbox,sbox string. Means that when you refresh your preprod, staging environments from production, you have to rename the target schema from prod_ to stg_ or something like. It is possible, but it is never easy."] + #[serde(skip_serializing_if = "Option::is_none")] + pub schema_prefixed_or_suffixed_with_envt: Option>, + #[doc = "SchemaWithDefaultRoleNotGranted (S001): The schema has no default role. Means that futur table will not be granted through a role. So you will have to re-execute grants on it."] + #[serde(skip_serializing_if = "Option::is_none")] + pub schema_with_default_role_not_granted: Option>, + #[doc = "UnsecuredPublicSchema (S003): Only authorized users should be allowed to create objects."] + #[serde(skip_serializing_if = "Option::is_none")] + pub unsecured_public_schema: Option>, +} +impl Schema { + const GROUP_NAME: &'static str = "schema"; + pub(crate) const GROUP_RULES: &'static [&'static str] = &[ + "ownerSchemaIsInternalRole", + "schemaOwnerDoNotMatchTableOwner", + "schemaPrefixedOrSuffixedWithEnvt", + "schemaWithDefaultRoleNotGranted", + "unsecuredPublicSchema", + ]; + const RECOMMENDED_RULES_AS_FILTERS: &'static [RuleFilter<'static>] = &[ + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[0]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[1]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[2]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[3]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[4]), + ]; + const ALL_RULES_AS_FILTERS: &'static [RuleFilter<'static>] = &[ + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[0]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[1]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[2]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[3]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[4]), + ]; + #[doc = r" Retrieves the recommended rules"] + pub(crate) fn is_recommended_true(&self) -> bool { + matches!(self.recommended, Some(true)) + } + pub(crate) fn is_recommended_unset(&self) -> bool { + self.recommended.is_none() + } + pub(crate) fn is_all_true(&self) -> bool { + matches!(self.all, Some(true)) + } + pub(crate) fn is_all_unset(&self) -> bool { + self.all.is_none() + } + pub(crate) fn get_enabled_rules(&self) -> FxHashSet> { + let mut index_set = FxHashSet::default(); + if let Some(rule) = self.owner_schema_is_internal_role.as_ref() { + if rule.is_enabled() { + index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[0])); + } + } + if let Some(rule) = self.schema_owner_do_not_match_table_owner.as_ref() { + if rule.is_enabled() { + index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[1])); + } + } + if let Some(rule) = self.schema_prefixed_or_suffixed_with_envt.as_ref() { + if rule.is_enabled() { + index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[2])); + } + } + if let Some(rule) = self.schema_with_default_role_not_granted.as_ref() { + if rule.is_enabled() { + index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[3])); + } + } + if let Some(rule) = self.unsecured_public_schema.as_ref() { + if rule.is_enabled() { + index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[4])); + } + } + index_set + } + pub(crate) fn get_disabled_rules(&self) -> FxHashSet> { + let mut index_set = FxHashSet::default(); + if let Some(rule) = self.owner_schema_is_internal_role.as_ref() { + if rule.is_disabled() { + index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[0])); + } + } + if let Some(rule) = self.schema_owner_do_not_match_table_owner.as_ref() { + if rule.is_disabled() { + index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[1])); + } + } + if let Some(rule) = self.schema_prefixed_or_suffixed_with_envt.as_ref() { + if rule.is_disabled() { + index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[2])); + } + } + if let Some(rule) = self.schema_with_default_role_not_granted.as_ref() { + if rule.is_disabled() { + index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[3])); + } + } + if let Some(rule) = self.unsecured_public_schema.as_ref() { + if rule.is_disabled() { + index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[4])); + } + } + index_set + } + #[doc = r" Checks if, given a rule name, matches one of the rules contained in this category"] + pub(crate) fn has_rule(rule_name: &str) -> Option<&'static str> { + Some(Self::GROUP_RULES[Self::GROUP_RULES.binary_search(&rule_name).ok()?]) + } + pub(crate) fn recommended_rules_as_filters() -> &'static [RuleFilter<'static>] { + Self::RECOMMENDED_RULES_AS_FILTERS + } + pub(crate) fn all_rules_as_filters() -> &'static [RuleFilter<'static>] { + Self::ALL_RULES_AS_FILTERS + } + #[doc = r" Select preset rules"] + pub(crate) fn collect_preset_rules( + &self, + parent_is_all: bool, + parent_is_recommended: bool, + enabled_rules: &mut FxHashSet>, + ) { + if self.is_all_true() || self.is_all_unset() && parent_is_all { + enabled_rules.extend(Self::all_rules_as_filters()); + } else if self.is_recommended_true() + || self.is_recommended_unset() && self.is_all_unset() && parent_is_recommended + { + enabled_rules.extend(Self::recommended_rules_as_filters()); + } + } + pub(crate) fn severity(rule_name: &str) -> Severity { + match rule_name { + "ownerSchemaIsInternalRole" => Severity::Warning, + "schemaOwnerDoNotMatchTableOwner" => Severity::Warning, + "schemaPrefixedOrSuffixedWithEnvt" => Severity::Warning, + "schemaWithDefaultRoleNotGranted" => Severity::Warning, + "unsecuredPublicSchema" => Severity::Warning, + _ => unreachable!(), + } + } + pub(crate) fn get_rule_configuration( + &self, + rule_name: &str, + ) -> Option<(RulePlainConfiguration, Option)> { + match rule_name { + "ownerSchemaIsInternalRole" => self + .owner_schema_is_internal_role + .as_ref() + .map(|conf| (conf.level(), conf.get_options())), + "schemaOwnerDoNotMatchTableOwner" => self + .schema_owner_do_not_match_table_owner + .as_ref() + .map(|conf| (conf.level(), conf.get_options())), + "schemaPrefixedOrSuffixedWithEnvt" => self + .schema_prefixed_or_suffixed_with_envt + .as_ref() + .map(|conf| (conf.level(), conf.get_options())), + "schemaWithDefaultRoleNotGranted" => self + .schema_with_default_role_not_granted + .as_ref() + .map(|conf| (conf.level(), conf.get_options())), + "unsecuredPublicSchema" => self + .unsecured_public_schema + .as_ref() + .map(|conf| (conf.level(), conf.get_options())), + _ => None, + } + } +} +#[doc = r" Push the configured rules to the analyser"] +pub fn push_to_analyser_rules( + rules: &Rules, + metadata: &pgls_analyse::MetadataRegistry, + analyser_rules: &mut pgls_analyser::LinterRules, +) { + if let Some(rules) = rules.base.as_ref() { + for rule_name in Base::GROUP_RULES { + if let Some((_, Some(rule_options))) = rules.get_rule_configuration(rule_name) { + if let Some(rule_key) = metadata.find_rule("base", rule_name) { + analyser_rules.push_rule(rule_key, rule_options); + } + } + } + } + if let Some(rules) = rules.cluster.as_ref() { + for rule_name in Cluster::GROUP_RULES { + if let Some((_, Some(rule_options))) = rules.get_rule_configuration(rule_name) { + if let Some(rule_key) = metadata.find_rule("cluster", rule_name) { + analyser_rules.push_rule(rule_key, rule_options); + } + } + } + } + if let Some(rules) = rules.schema.as_ref() { + for rule_name in Schema::GROUP_RULES { + if let Some((_, Some(rule_options))) = rules.get_rule_configuration(rule_name) { + if let Some(rule_key) = metadata.find_rule("schema", rule_name) { + analyser_rules.push_rule(rule_key, rule_options); + } + } + } + } +} +#[test] +fn test_order() { + for items in Base::GROUP_RULES.windows(2) { + assert!(items[0] < items[1], "{} < {}", items[0], items[1]); + } + for items in Cluster::GROUP_RULES.windows(2) { + assert!(items[0] < items[1], "{} < {}", items[0], items[1]); + } + for items in Schema::GROUP_RULES.windows(2) { + assert!(items[0] < items[1], "{} < {}", items[0], items[1]); + } +} diff --git a/crates/pgls_configuration/src/rules/selector.rs b/crates/pgls_configuration/src/rules/selector.rs index 4627e2388..e206fff25 100644 --- a/crates/pgls_configuration/src/rules/selector.rs +++ b/crates/pgls_configuration/src/rules/selector.rs @@ -2,11 +2,12 @@ use pgls_analyse::RuleFilter; use std::str::FromStr; -/// Represents a rule group from any analyzer (linter or splinter) +/// Represents a rule group from any analyzer (linter, splinter, or pglinter) #[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)] pub enum AnalyzerGroup { Linter(crate::linter::RuleGroup), Splinter(crate::splinter::RuleGroup), + PgLinter(crate::pglinter::RuleGroup), } impl AnalyzerGroup { @@ -14,6 +15,7 @@ impl AnalyzerGroup { match self { Self::Linter(group) => group.as_str(), Self::Splinter(group) => group.as_str(), + Self::PgLinter(group) => group.as_str(), } } @@ -21,6 +23,7 @@ impl AnalyzerGroup { match self { Self::Linter(_) => "lint", Self::Splinter(_) => "splinter", + Self::PgLinter(_) => "pglinter", } } } @@ -57,6 +60,8 @@ impl FromStr for RuleSelector { ("lint", rest) } else if let Some(rest) = selector.strip_prefix("splinter/") { ("splinter", rest) + } else if let Some(rest) = selector.strip_prefix("pglinter/") { + ("pglinter", rest) } else { // Default to lint for backward compatibility ("lint", selector) @@ -84,6 +89,17 @@ impl FromStr for RuleSelector { Err("This rule doesn't exist.") } } + "pglinter" => { + let group = crate::pglinter::RuleGroup::from_str(group_name)?; + if let Some(rule_name) = crate::pglinter::Rules::has_rule(group, rule_name) { + Ok(RuleSelector::Rule( + AnalyzerGroup::PgLinter(group), + rule_name, + )) + } else { + Err("This rule doesn't exist.") + } + } _ => Err("Unknown analyzer type."), } } else { @@ -101,6 +117,12 @@ impl FromStr for RuleSelector { "This group doesn't exist. Use the syntax `/` to specify a rule.", ), }, + "pglinter" => match crate::pglinter::RuleGroup::from_str(rest) { + Ok(group) => Ok(RuleSelector::Group(AnalyzerGroup::PgLinter(group))), + Err(_) => Err( + "This group doesn't exist. Use the syntax `/` to specify a rule.", + ), + }, _ => Err("Unknown analyzer type."), } } diff --git a/crates/pgls_diagnostics_categories/src/categories.rs b/crates/pgls_diagnostics_categories/src/categories.rs index ab22178f0..a2756af45 100644 --- a/crates/pgls_diagnostics_categories/src/categories.rs +++ b/crates/pgls_diagnostics_categories/src/categories.rs @@ -47,6 +47,35 @@ define_categories! { "lint/safety/runningStatementWhileHoldingAccessExclusive": "https://pg-language-server.com/latest/reference/rules/running-statement-while-holding-access-exclusive/", "lint/safety/transactionNesting": "https://pg-language-server.com/latest/reference/rules/transaction-nesting/", // end lint rules + // pglinter rules start + // Meta diagnostics + "pglinter/extensionNotInstalled": "Install the pglinter extension with: CREATE EXTENSION pglinter", + "pglinter/ruleDisabledInExtension": "Enable the rule in the extension with: UPDATE pglinter.rules SET enable = true WHERE code = ''", + // Base rules (B-series) + "pglinter/base/compositePrimaryKeyTooManyColumns": "https://github.com/pmpetit/pglinter#b012", + "pglinter/base/howManyObjectsWithUppercase": "https://github.com/pmpetit/pglinter#b005", + "pglinter/base/howManyRedudantIndex": "https://github.com/pmpetit/pglinter#b002", + "pglinter/base/howManyTableWithoutIndexOnFk": "https://github.com/pmpetit/pglinter#b003", + "pglinter/base/howManyTableWithoutPrimaryKey": "https://github.com/pmpetit/pglinter#b001", + "pglinter/base/howManyTablesNeverSelected": "https://github.com/pmpetit/pglinter#b006", + "pglinter/base/howManyTablesWithFkMismatch": "https://github.com/pmpetit/pglinter#b008", + "pglinter/base/howManyTablesWithFkOutsideSchema": "https://github.com/pmpetit/pglinter#b007", + "pglinter/base/howManyTablesWithReservedKeywords": "https://github.com/pmpetit/pglinter#b010", + "pglinter/base/howManyTablesWithSameTrigger": "https://github.com/pmpetit/pglinter#b009", + "pglinter/base/howManyUnusedIndex": "https://github.com/pmpetit/pglinter#b004", + "pglinter/base/severalTableOwnerInSchema": "https://github.com/pmpetit/pglinter#b011", + // Cluster rules (C-series) + "pglinter/cluster/passwordEncryptionIsMd5": "https://github.com/pmpetit/pglinter#c003", + "pglinter/cluster/pgHbaEntriesWithMethodTrustOrPasswordShouldNotExists": "https://github.com/pmpetit/pglinter#c002", + "pglinter/cluster/pgHbaEntriesWithMethodTrustShouldNotExists": "https://github.com/pmpetit/pglinter#c001", + // Schema rules (S-series) + "pglinter/schema/ownerSchemaIsInternalRole": "https://github.com/pmpetit/pglinter#s004", + "pglinter/schema/schemaOwnerDoNotMatchTableOwner": "https://github.com/pmpetit/pglinter#s005", + "pglinter/schema/schemaPrefixedOrSuffixedWithEnvt": "https://github.com/pmpetit/pglinter#s002", + "pglinter/schema/schemaWithDefaultRoleNotGranted": "https://github.com/pmpetit/pglinter#s001", + "pglinter/schema/unsecuredPublicSchema": "https://github.com/pmpetit/pglinter#s003", + // pglinter rules end + // splinter rules start "splinter/performance/authRlsInitplan": "https://supabase.com/docs/guides/database/database-linter?lint=0003_auth_rls_initplan", "splinter/performance/duplicateIndex": "https://supabase.com/docs/guides/database/database-linter?lint=0009_duplicate_index", @@ -99,4 +128,11 @@ define_categories! { "splinter/performance", "splinter/security", // Splinter groups end + + // Pglinter groups start + "pglinter", + "pglinter/base", + "pglinter/cluster", + "pglinter/schema", + // Pglinter groups end } diff --git a/crates/pgls_pglinter/Cargo.toml b/crates/pgls_pglinter/Cargo.toml new file mode 100644 index 000000000..983e00a8f --- /dev/null +++ b/crates/pgls_pglinter/Cargo.toml @@ -0,0 +1,29 @@ +[package] +authors.workspace = true +categories.workspace = true +description = "pglinter Postgres extension integration for database linting" +edition.workspace = true +homepage.workspace = true +keywords.workspace = true +license.workspace = true +name = "pgls_pglinter" +repository.workspace = true +version = "0.0.0" + +[dependencies] +pgls_analyse.workspace = true +pgls_diagnostics.workspace = true +pgls_diagnostics_categories.workspace = true +pgls_schema_cache.workspace = true +rustc-hash.workspace = true +serde.workspace = true +serde_json.workspace = true +sqlx.workspace = true + +[dev-dependencies] +insta.workspace = true +pgls_console.workspace = true +pgls_test_utils.workspace = true + +[lib] +doctest = false diff --git a/crates/pgls_pglinter/src/cache.rs b/crates/pgls_pglinter/src/cache.rs new file mode 100644 index 000000000..015fd73f2 --- /dev/null +++ b/crates/pgls_pglinter/src/cache.rs @@ -0,0 +1,58 @@ +//! Pglinter extension cache for avoiding repeated database queries + +use pgls_schema_cache::SchemaCache; +use rustc_hash::FxHashSet; +use sqlx::PgPool; + +/// Cached pglinter extension state (loaded once, reused) +#[derive(Debug, Clone, Default)] +pub struct PglinterCache { + /// Whether the pglinter extension is installed + pub extension_installed: bool, + /// Rule codes that are disabled in the pglinter extension + pub disabled_rules: FxHashSet, +} + +impl PglinterCache { + /// Load pglinter extension state from database using official API + pub async fn load(conn: &PgPool, schema_cache: &SchemaCache) -> Result { + let extension_installed = schema_cache.extensions.iter().any(|e| e.name == "pglinter"); + + if !extension_installed { + return Ok(Self { + extension_installed: false, + disabled_rules: FxHashSet::default(), + }); + } + + // Get disabled rules using pglinter.show_rules() - single query + let disabled_rules = get_disabled_rules(conn).await?; + + Ok(Self { + extension_installed, + disabled_rules, + }) + } + + /// Create initial cache from schema cache only (disabled rules will need API call later) + pub fn from_schema_cache(schema_cache: &SchemaCache) -> Self { + Self { + extension_installed: schema_cache.extensions.iter().any(|e| e.name == "pglinter"), + disabled_rules: FxHashSet::default(), + } + } +} + +/// Get disabled rules using pglinter's official API: pglinter.show_rules() +pub async fn get_disabled_rules(conn: &PgPool) -> Result, sqlx::Error> { + let rows: Vec<(String, bool)> = + sqlx::query_as("SELECT rule_code, enabled FROM pglinter.show_rules()") + .fetch_all(conn) + .await?; + + Ok(rows + .into_iter() + .filter(|(_, enabled)| !enabled) + .map(|(code, _)| code) + .collect()) +} diff --git a/crates/pgls_pglinter/src/diagnostics.rs b/crates/pgls_pglinter/src/diagnostics.rs new file mode 100644 index 000000000..ad19c81fc --- /dev/null +++ b/crates/pgls_pglinter/src/diagnostics.rs @@ -0,0 +1,183 @@ +//! Pglinter diagnostic types and conversion from SARIF + +use pgls_diagnostics::{ + Advices, Category, DatabaseObjectOwned, Diagnostic, LogCategory, MessageAndDescription, + Severity, Visit, +}; +use std::io; + +use crate::sarif; + +/// A specialized diagnostic for pglinter (database-level linting via pglinter extension). +#[derive(Debug, Diagnostic, PartialEq)] +pub struct PglinterDiagnostic { + #[category] + pub category: &'static Category, + + #[location(database_object)] + pub db_object: Option, + + #[message] + #[description] + pub message: MessageAndDescription, + + #[severity] + pub severity: Severity, + + #[advice] + pub advices: PglinterAdvices, +} + +/// Advices for pglinter diagnostics +#[derive(Debug, PartialEq)] +pub struct PglinterAdvices { + /// General description of what this rule detects + pub description: String, + + /// Rule code (e.g., "B001", "S001", "C001") + pub rule_code: Option, + + /// Suggested fixes for the issue + pub fixes: Vec, + + /// List of affected database objects + pub object_list: Option, +} + +impl Advices for PglinterAdvices { + fn record(&self, visitor: &mut dyn Visit) -> io::Result<()> { + if !self.description.is_empty() { + visitor.record_log(LogCategory::None, &self.description)?; + } + + if let Some(code) = &self.rule_code { + visitor.record_log(LogCategory::Info, &format!("Rule: {code}"))?; + } + + if let Some(objects) = &self.object_list { + if !objects.is_empty() { + visitor.record_log(LogCategory::None, &"Affected objects:")?; + for line in objects.lines() { + visitor.record_log(LogCategory::Info, &format!(" {line}"))?; + } + } + } + + if !self.fixes.is_empty() { + visitor.record_log(LogCategory::None, &"How to fix:")?; + for (i, fix) in self.fixes.iter().enumerate() { + let num = i + 1; + visitor.record_log(LogCategory::Info, &format!(" {num}. {fix}"))?; + } + } + + Ok(()) + } +} + +/// Error when converting SARIF to diagnostics +#[derive(Debug)] +pub struct UnknownRuleError { + pub rule_code: String, +} + +impl std::fmt::Display for UnknownRuleError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "Unknown pglinter rule code: {}", self.rule_code) + } +} + +impl std::error::Error for UnknownRuleError {} + +impl PglinterDiagnostic { + /// Try to convert a single SARIF result to a pglinter diagnostic + pub fn try_from_sarif( + result: &sarif::Result, + rule_code: &str, + ) -> Result { + let category = + crate::registry::get_rule_category(rule_code).ok_or_else(|| UnknownRuleError { + rule_code: rule_code.to_string(), + })?; + + let metadata = crate::registry::get_rule_metadata_by_code(rule_code); + + let severity = match result.level_str() { + "error" => Severity::Error, + "warning" => Severity::Warning, + "note" => Severity::Information, + _ => Severity::Warning, + }; + + let message = result.message_text().to_string(); + let description = metadata + .map(|m| m.description.to_string()) + .unwrap_or_else(|| message.clone()); + + let fixes = metadata + .map(|m| m.fixes.iter().map(|s| s.to_string()).collect()) + .unwrap_or_default(); + + let object_list = { + let names = result.logical_location_names(); + if names.is_empty() { + None + } else { + Some(names.join("\n")) + } + }; + + Ok(PglinterDiagnostic { + category, + db_object: None, + message: message.into(), + severity, + advices: PglinterAdvices { + description, + rule_code: Some(rule_code.to_string()), + fixes, + object_list, + }, + }) + } + + /// Create diagnostic for missing pglinter extension + pub fn extension_not_installed() -> PglinterDiagnostic { + PglinterDiagnostic { + category: pgls_diagnostics::category!("pglinter/extensionNotInstalled"), + db_object: None, + message: "The pglinter extension is not installed in the database. Install it with 'CREATE EXTENSION pglinter' or disable pglinter rules in your configuration.".into(), + severity: Severity::Error, + advices: PglinterAdvices { + description: "pglinter rules are enabled in your configuration but the extension is not installed.".to_string(), + rule_code: None, + fixes: vec!["Install the pglinter extension: CREATE EXTENSION pglinter".to_string()], + object_list: None, + }, + } + } + + /// Create diagnostic for rule disabled in pglinter extension + pub fn rule_disabled_in_extension(rule_code: &str) -> PglinterDiagnostic { + let description = format!( + "Rule {rule_code} is enabled in configuration but disabled in pglinter extension. Enable it with: SELECT pglinter.enable_rule('{rule_code}')" + ); + + PglinterDiagnostic { + category: pgls_diagnostics::category!("pglinter/ruleDisabledInExtension"), + db_object: None, + message: description.into(), + severity: Severity::Error, + advices: PglinterAdvices { + description: format!( + "Rule {rule_code} is configured to run but is disabled in the pglinter extension." + ), + rule_code: Some(rule_code.to_string()), + fixes: vec![format!( + "Enable the rule: SELECT pglinter.enable_rule('{rule_code}')" + )], + object_list: None, + }, + } + } +} diff --git a/crates/pgls_pglinter/src/lib.rs b/crates/pgls_pglinter/src/lib.rs new file mode 100644 index 000000000..8506a70e2 --- /dev/null +++ b/crates/pgls_pglinter/src/lib.rs @@ -0,0 +1,157 @@ +//! pglinter Postgres extension integration for database linting + +mod cache; +mod diagnostics; +pub mod registry; +pub mod rule; +pub mod rules; +pub mod sarif; + +use pgls_analyse::{AnalysisFilter, RegistryVisitor, RuleMeta}; +use pgls_schema_cache::SchemaCache; +use sqlx::PgPool; + +pub use cache::PglinterCache; +pub use diagnostics::{PglinterAdvices, PglinterDiagnostic}; +pub use rule::PglinterRule; +pub use sarif::SarifLog; + +/// Parameters for running pglinter +#[derive(Debug)] +pub struct PglinterParams<'a> { + pub conn: &'a PgPool, + pub schema_cache: &'a SchemaCache, +} + +/// Visitor that collects enabled pglinter rules based on filter +struct RuleCollector<'a> { + filter: &'a AnalysisFilter<'a>, + enabled_rules: Vec, +} + +impl<'a> RegistryVisitor for RuleCollector<'a> { + fn record_category(&mut self) { + if self.filter.match_category::() { + C::record_groups(self); + } + } + + fn record_group(&mut self) { + if self.filter.match_group::() { + G::record_rules(self); + } + } + + fn record_rule(&mut self) { + if self.filter.match_rule::() { + if let Some(code) = registry::get_rule_code(R::METADATA.name) { + self.enabled_rules.push(code.to_string()); + } + } + } +} + +fn collect_enabled_rules(filter: &AnalysisFilter<'_>) -> Vec { + let mut collector = RuleCollector { + filter, + enabled_rules: Vec::new(), + }; + registry::visit_registry(&mut collector); + collector.enabled_rules +} + +/// Run pglinter rules against the database +pub async fn run_pglinter( + params: PglinterParams<'_>, + filter: &AnalysisFilter<'_>, + cache: Option<&PglinterCache>, +) -> Result, sqlx::Error> { + let mut results = vec![]; + + // Check extension installed + let extension_installed = cache.map(|c| c.extension_installed).unwrap_or_else(|| { + params + .schema_cache + .extensions + .iter() + .any(|e| e.name == "pglinter") + }); + + // Collect enabled rules from config + let enabled_rules = collect_enabled_rules(filter); + + if !extension_installed { + if !enabled_rules.is_empty() { + results.push(PglinterDiagnostic::extension_not_installed()); + } + return Ok(results); + } + + if enabled_rules.is_empty() { + return Ok(results); + } + + // Get disabled rules from extension + let disabled_in_extension = match cache { + Some(c) => c.disabled_rules.clone(), + None => cache::get_disabled_rules(params.conn).await?, + }; + + // Check for mismatches and collect runnable rules + let mut runnable_rules = Vec::new(); + for rule_code in &enabled_rules { + if disabled_in_extension.contains(rule_code) { + results.push(PglinterDiagnostic::rule_disabled_in_extension(rule_code)); + } else { + runnable_rules.push(rule_code.clone()); + } + } + + if runnable_rules.is_empty() { + return Ok(results); + } + + // Execute each rule + for rule_code in &runnable_rules { + if let Some(diags) = execute_rule(params.conn, rule_code).await? { + results.extend(diags); + } + } + + Ok(results) +} + +/// Execute a single pglinter rule using pglinter.check_rule() +async fn execute_rule( + conn: &PgPool, + rule_code: &str, +) -> Result>, sqlx::Error> { + let result: Option = sqlx::query_scalar("SELECT pglinter.check_rule($1)") + .bind(rule_code) + .fetch_optional(conn) + .await?; + + let Some(sarif_json) = result else { + return Ok(None); + }; + + let sarif = match SarifLog::parse(&sarif_json) { + Ok(s) => s, + Err(_) => return Ok(None), + }; + + if !sarif.has_results() { + return Ok(None); + } + + let diags: Vec<_> = sarif + .all_results() + .filter_map(|result| PglinterDiagnostic::try_from_sarif(result, rule_code).ok()) + .collect(); + + if diags.is_empty() { + Ok(None) + } else { + Ok(Some(diags)) + } +} diff --git a/crates/pgls_pglinter/src/registry.rs b/crates/pgls_pglinter/src/registry.rs new file mode 100644 index 000000000..1ec8516b6 --- /dev/null +++ b/crates/pgls_pglinter/src/registry.rs @@ -0,0 +1,463 @@ +//! Generated file, do not edit by hand, see `xtask/codegen` + +#![doc = r" Generated file, do not edit by hand, see `xtask/codegen`"] +use pgls_analyse::RegistryVisitor; +use pgls_diagnostics::Category; +#[doc = r" Metadata for a pglinter rule"] +#[derive(Debug, Clone, Copy)] +pub struct RuleMetadata { + #[doc = r#" Rule code (e.g., "B001")"#] + pub code: &'static str, + #[doc = r" Rule name in camelCase"] + pub name: &'static str, + #[doc = r" Rule scope (BASE, SCHEMA, CLUSTER)"] + pub scope: &'static str, + #[doc = r" Description of what the rule detects"] + pub description: &'static str, + #[doc = r" Suggested fixes"] + pub fixes: &'static [&'static str], +} +#[doc = r" Visit all pglinter rules using the visitor pattern"] +pub fn visit_registry(registry: &mut V) { + registry.record_category::(); +} +#[doc = r" Get the pglinter rule code from the camelCase name"] +pub fn get_rule_code(name: &str) -> Option<&'static str> { + match name { + "compositePrimaryKeyTooManyColumns" => Some("B012"), + "howManyObjectsWithUppercase" => Some("B005"), + "howManyRedudantIndex" => Some("B002"), + "howManyTableWithoutIndexOnFk" => Some("B003"), + "howManyTableWithoutPrimaryKey" => Some("B001"), + "howManyTablesNeverSelected" => Some("B006"), + "howManyTablesWithFkMismatch" => Some("B008"), + "howManyTablesWithFkOutsideSchema" => Some("B007"), + "howManyTablesWithReservedKeywords" => Some("B010"), + "howManyTablesWithSameTrigger" => Some("B009"), + "howManyUnusedIndex" => Some("B004"), + "ownerSchemaIsInternalRole" => Some("S004"), + "passwordEncryptionIsMd5" => Some("C003"), + "pgHbaEntriesWithMethodTrustOrPasswordShouldNotExists" => Some("C002"), + "pgHbaEntriesWithMethodTrustShouldNotExists" => Some("C001"), + "schemaOwnerDoNotMatchTableOwner" => Some("S005"), + "schemaPrefixedOrSuffixedWithEnvt" => Some("S002"), + "schemaWithDefaultRoleNotGranted" => Some("S001"), + "severalTableOwnerInSchema" => Some("B011"), + "unsecuredPublicSchema" => Some("S003"), + _ => None, + } +} +#[doc = r" Get the diagnostic category for a rule code"] +pub fn get_rule_category(code: &str) -> Option<&'static Category> { + match code { + "B012" => Some(::pgls_diagnostics::category!( + "pglinter/base/compositePrimaryKeyTooManyColumns" + )), + "B005" => Some(::pgls_diagnostics::category!( + "pglinter/base/howManyObjectsWithUppercase" + )), + "B002" => Some(::pgls_diagnostics::category!( + "pglinter/base/howManyRedudantIndex" + )), + "B003" => Some(::pgls_diagnostics::category!( + "pglinter/base/howManyTableWithoutIndexOnFk" + )), + "B001" => Some(::pgls_diagnostics::category!( + "pglinter/base/howManyTableWithoutPrimaryKey" + )), + "B006" => Some(::pgls_diagnostics::category!( + "pglinter/base/howManyTablesNeverSelected" + )), + "B008" => Some(::pgls_diagnostics::category!( + "pglinter/base/howManyTablesWithFkMismatch" + )), + "B007" => Some(::pgls_diagnostics::category!( + "pglinter/base/howManyTablesWithFkOutsideSchema" + )), + "B010" => Some(::pgls_diagnostics::category!( + "pglinter/base/howManyTablesWithReservedKeywords" + )), + "B009" => Some(::pgls_diagnostics::category!( + "pglinter/base/howManyTablesWithSameTrigger" + )), + "B004" => Some(::pgls_diagnostics::category!( + "pglinter/base/howManyUnusedIndex" + )), + "S004" => Some(::pgls_diagnostics::category!( + "pglinter/schema/ownerSchemaIsInternalRole" + )), + "C003" => Some(::pgls_diagnostics::category!( + "pglinter/cluster/passwordEncryptionIsMd5" + )), + "C002" => Some(::pgls_diagnostics::category!( + "pglinter/cluster/pgHbaEntriesWithMethodTrustOrPasswordShouldNotExists" + )), + "C001" => Some(::pgls_diagnostics::category!( + "pglinter/cluster/pgHbaEntriesWithMethodTrustShouldNotExists" + )), + "S005" => Some(::pgls_diagnostics::category!( + "pglinter/schema/schemaOwnerDoNotMatchTableOwner" + )), + "S002" => Some(::pgls_diagnostics::category!( + "pglinter/schema/schemaPrefixedOrSuffixedWithEnvt" + )), + "S001" => Some(::pgls_diagnostics::category!( + "pglinter/schema/schemaWithDefaultRoleNotGranted" + )), + "B011" => Some(::pgls_diagnostics::category!( + "pglinter/base/severalTableOwnerInSchema" + )), + "S003" => Some(::pgls_diagnostics::category!( + "pglinter/schema/unsecuredPublicSchema" + )), + _ => None, + } +} +#[doc = r" Get rule metadata by name (camelCase)"] +pub fn get_rule_metadata(name: &str) -> Option { + match name { + "compositePrimaryKeyTooManyColumns" => Some(RuleMetadata { + code: "B012", + name: "compositePrimaryKeyTooManyColumns", + scope: "BASE", + description: "Detect tables with composite primary keys involving more than 4 columns", + fixes: &[ + "Consider redesigning the table to avoid composite primary keys with more than 4 columns", + "Use surrogate keys (e.g., serial, UUID) instead of composite primary keys, and establish unique constraints on necessary column combinations, to enforce uniqueness.", + ], + }), + "howManyObjectsWithUppercase" => Some(RuleMetadata { + code: "B005", + name: "howManyObjectsWithUppercase", + scope: "BASE", + description: "Count number of objects with uppercase in name or in columns.", + fixes: &["Do not use uppercase for any database objects"], + }), + "howManyRedudantIndex" => Some(RuleMetadata { + code: "B002", + name: "howManyRedudantIndex", + scope: "BASE", + description: "Count number of redundant index vs nb index.", + fixes: &[ + "remove duplicated index or check if a constraint does not create a redundant index, or change warning/error threshold", + ], + }), + "howManyTableWithoutIndexOnFk" => Some(RuleMetadata { + code: "B003", + name: "howManyTableWithoutIndexOnFk", + scope: "BASE", + description: "Count number of tables without index on foreign key.", + fixes: &["create a index on foreign key or change warning/error threshold"], + }), + "howManyTableWithoutPrimaryKey" => Some(RuleMetadata { + code: "B001", + name: "howManyTableWithoutPrimaryKey", + scope: "BASE", + description: "Count number of tables without primary key.", + fixes: &["create a primary key or change warning/error threshold"], + }), + "howManyTablesNeverSelected" => Some(RuleMetadata { + code: "B006", + name: "howManyTablesNeverSelected", + scope: "BASE", + description: "Count number of table(s) that has never been selected.", + fixes: &[ + "Is it necessary to update/delete/insert rows in table(s) that are never selected ?", + ], + }), + "howManyTablesWithFkMismatch" => Some(RuleMetadata { + code: "B008", + name: "howManyTablesWithFkMismatch", + scope: "BASE", + description: "Count number of tables with foreign keys that do not match the key reference type.", + fixes: &[ + "Consider column type adjustments to ensure foreign key matches referenced key type", + "ask a dba", + ], + }), + "howManyTablesWithFkOutsideSchema" => Some(RuleMetadata { + code: "B007", + name: "howManyTablesWithFkOutsideSchema", + scope: "BASE", + description: "Count number of tables with foreign keys outside their schema.", + fixes: &[ + "Consider restructuring schema design to keep related tables in same schema", + "ask a dba", + ], + }), + "howManyTablesWithReservedKeywords" => Some(RuleMetadata { + code: "B010", + name: "howManyTablesWithReservedKeywords", + scope: "BASE", + description: "Count number of database objects using reserved keywords in their names.", + fixes: &[ + "Rename database objects to avoid using reserved keywords.", + "Using reserved keywords can lead to SQL syntax errors and maintenance difficulties.", + ], + }), + "howManyTablesWithSameTrigger" => Some(RuleMetadata { + code: "B009", + name: "howManyTablesWithSameTrigger", + scope: "BASE", + description: "Count number of tables using the same trigger vs nb table with their own triggers.", + fixes: &[ + "For more readability and other considerations use one trigger function per table.", + "Sharing the same trigger function add more complexity.", + ], + }), + "howManyUnusedIndex" => Some(RuleMetadata { + code: "B004", + name: "howManyUnusedIndex", + scope: "BASE", + description: "Count number of unused index vs nb index (base on pg_stat_user_indexes, indexes associated to unique constraints are discard.)", + fixes: &["remove unused index or change warning/error threshold"], + }), + "ownerSchemaIsInternalRole" => Some(RuleMetadata { + code: "S004", + name: "ownerSchemaIsInternalRole", + scope: "SCHEMA", + description: "Owner of schema should not be any internal pg roles, or owner is a superuser (not sure it is necesary).", + fixes: &["change schema owner to a functional role"], + }), + "passwordEncryptionIsMd5" => Some(RuleMetadata { + code: "C003", + name: "passwordEncryptionIsMd5", + scope: "CLUSTER", + description: "This configuration is not secure anymore and will prevent an upgrade to Postgres 18. Warning, you will need to reset all passwords after this is changed to scram-sha-256.", + fixes: &[ + "change password_encryption parameter to scram-sha-256 (ALTER SYSTEM SET password_encryption = ", + "scram-sha-256", + " ). Warning, you will need to reset all passwords after this parameter is updated.", + ], + }), + "pgHbaEntriesWithMethodTrustOrPasswordShouldNotExists" => Some(RuleMetadata { + code: "C002", + name: "pgHbaEntriesWithMethodTrustOrPasswordShouldNotExists", + scope: "CLUSTER", + description: "This configuration is extremely insecure and should only be used in a controlled, non-production environment for testing purposes. In a production environment, you should use more secure authentication methods such as md5, scram-sha-256, or cert, and restrict access to trusted IP addresses only.", + fixes: &["change trust or password method in pg_hba.conf"], + }), + "pgHbaEntriesWithMethodTrustShouldNotExists" => Some(RuleMetadata { + code: "C001", + name: "pgHbaEntriesWithMethodTrustShouldNotExists", + scope: "CLUSTER", + description: "This configuration is extremely insecure and should only be used in a controlled, non-production environment for testing purposes. In a production environment, you should use more secure authentication methods such as md5, scram-sha-256, or cert, and restrict access to trusted IP addresses only.", + fixes: &["change trust method in pg_hba.conf"], + }), + "schemaOwnerDoNotMatchTableOwner" => Some(RuleMetadata { + code: "S005", + name: "schemaOwnerDoNotMatchTableOwner", + scope: "SCHEMA", + description: "The schema owner and tables in the schema do not match.", + fixes: &["For maintenance facilities, schema and tables owners should be the same."], + }), + "schemaPrefixedOrSuffixedWithEnvt" => Some(RuleMetadata { + code: "S002", + name: "schemaPrefixedOrSuffixedWithEnvt", + scope: "SCHEMA", + description: "The schema is prefixed with one of staging,stg,preprod,prod,sandbox,sbox string. Means that when you refresh your preprod, staging environments from production, you have to rename the target schema from prod_ to stg_ or something like. It is possible, but it is never easy.", + fixes: &[ + "Keep the same schema name across environments. Prefer prefix or suffix the database name", + ], + }), + "schemaWithDefaultRoleNotGranted" => Some(RuleMetadata { + code: "S001", + name: "schemaWithDefaultRoleNotGranted", + scope: "SCHEMA", + description: "The schema has no default role. Means that futur table will not be granted through a role. So you will have to re-execute grants on it.", + fixes: &[ + "add a default privilege=> ALTER DEFAULT PRIVILEGES IN SCHEMA for user ", + ], + }), + "severalTableOwnerInSchema" => Some(RuleMetadata { + code: "B011", + name: "severalTableOwnerInSchema", + scope: "BASE", + description: "In a schema there are several tables owned by different owners.", + fixes: &["change table owners to the same functional role"], + }), + "unsecuredPublicSchema" => Some(RuleMetadata { + code: "S003", + name: "unsecuredPublicSchema", + scope: "SCHEMA", + description: "Only authorized users should be allowed to create objects.", + fixes: &["REVOKE CREATE ON SCHEMA FROM PUBLIC"], + }), + _ => None, + } +} +#[doc = r#" Get rule metadata by code (e.g., "B001", "S001", "C001")"#] +pub fn get_rule_metadata_by_code(code: &str) -> Option { + match code { + "B012" => Some(RuleMetadata { + code: "B012", + name: "compositePrimaryKeyTooManyColumns", + scope: "BASE", + description: "Detect tables with composite primary keys involving more than 4 columns", + fixes: &[ + "Consider redesigning the table to avoid composite primary keys with more than 4 columns", + "Use surrogate keys (e.g., serial, UUID) instead of composite primary keys, and establish unique constraints on necessary column combinations, to enforce uniqueness.", + ], + }), + "B005" => Some(RuleMetadata { + code: "B005", + name: "howManyObjectsWithUppercase", + scope: "BASE", + description: "Count number of objects with uppercase in name or in columns.", + fixes: &["Do not use uppercase for any database objects"], + }), + "B002" => Some(RuleMetadata { + code: "B002", + name: "howManyRedudantIndex", + scope: "BASE", + description: "Count number of redundant index vs nb index.", + fixes: &[ + "remove duplicated index or check if a constraint does not create a redundant index, or change warning/error threshold", + ], + }), + "B003" => Some(RuleMetadata { + code: "B003", + name: "howManyTableWithoutIndexOnFk", + scope: "BASE", + description: "Count number of tables without index on foreign key.", + fixes: &["create a index on foreign key or change warning/error threshold"], + }), + "B001" => Some(RuleMetadata { + code: "B001", + name: "howManyTableWithoutPrimaryKey", + scope: "BASE", + description: "Count number of tables without primary key.", + fixes: &["create a primary key or change warning/error threshold"], + }), + "B006" => Some(RuleMetadata { + code: "B006", + name: "howManyTablesNeverSelected", + scope: "BASE", + description: "Count number of table(s) that has never been selected.", + fixes: &[ + "Is it necessary to update/delete/insert rows in table(s) that are never selected ?", + ], + }), + "B008" => Some(RuleMetadata { + code: "B008", + name: "howManyTablesWithFkMismatch", + scope: "BASE", + description: "Count number of tables with foreign keys that do not match the key reference type.", + fixes: &[ + "Consider column type adjustments to ensure foreign key matches referenced key type", + "ask a dba", + ], + }), + "B007" => Some(RuleMetadata { + code: "B007", + name: "howManyTablesWithFkOutsideSchema", + scope: "BASE", + description: "Count number of tables with foreign keys outside their schema.", + fixes: &[ + "Consider restructuring schema design to keep related tables in same schema", + "ask a dba", + ], + }), + "B010" => Some(RuleMetadata { + code: "B010", + name: "howManyTablesWithReservedKeywords", + scope: "BASE", + description: "Count number of database objects using reserved keywords in their names.", + fixes: &[ + "Rename database objects to avoid using reserved keywords.", + "Using reserved keywords can lead to SQL syntax errors and maintenance difficulties.", + ], + }), + "B009" => Some(RuleMetadata { + code: "B009", + name: "howManyTablesWithSameTrigger", + scope: "BASE", + description: "Count number of tables using the same trigger vs nb table with their own triggers.", + fixes: &[ + "For more readability and other considerations use one trigger function per table.", + "Sharing the same trigger function add more complexity.", + ], + }), + "B004" => Some(RuleMetadata { + code: "B004", + name: "howManyUnusedIndex", + scope: "BASE", + description: "Count number of unused index vs nb index (base on pg_stat_user_indexes, indexes associated to unique constraints are discard.)", + fixes: &["remove unused index or change warning/error threshold"], + }), + "S004" => Some(RuleMetadata { + code: "S004", + name: "ownerSchemaIsInternalRole", + scope: "SCHEMA", + description: "Owner of schema should not be any internal pg roles, or owner is a superuser (not sure it is necesary).", + fixes: &["change schema owner to a functional role"], + }), + "C003" => Some(RuleMetadata { + code: "C003", + name: "passwordEncryptionIsMd5", + scope: "CLUSTER", + description: "This configuration is not secure anymore and will prevent an upgrade to Postgres 18. Warning, you will need to reset all passwords after this is changed to scram-sha-256.", + fixes: &[ + "change password_encryption parameter to scram-sha-256 (ALTER SYSTEM SET password_encryption = ", + "scram-sha-256", + " ). Warning, you will need to reset all passwords after this parameter is updated.", + ], + }), + "C002" => Some(RuleMetadata { + code: "C002", + name: "pgHbaEntriesWithMethodTrustOrPasswordShouldNotExists", + scope: "CLUSTER", + description: "This configuration is extremely insecure and should only be used in a controlled, non-production environment for testing purposes. In a production environment, you should use more secure authentication methods such as md5, scram-sha-256, or cert, and restrict access to trusted IP addresses only.", + fixes: &["change trust or password method in pg_hba.conf"], + }), + "C001" => Some(RuleMetadata { + code: "C001", + name: "pgHbaEntriesWithMethodTrustShouldNotExists", + scope: "CLUSTER", + description: "This configuration is extremely insecure and should only be used in a controlled, non-production environment for testing purposes. In a production environment, you should use more secure authentication methods such as md5, scram-sha-256, or cert, and restrict access to trusted IP addresses only.", + fixes: &["change trust method in pg_hba.conf"], + }), + "S005" => Some(RuleMetadata { + code: "S005", + name: "schemaOwnerDoNotMatchTableOwner", + scope: "SCHEMA", + description: "The schema owner and tables in the schema do not match.", + fixes: &["For maintenance facilities, schema and tables owners should be the same."], + }), + "S002" => Some(RuleMetadata { + code: "S002", + name: "schemaPrefixedOrSuffixedWithEnvt", + scope: "SCHEMA", + description: "The schema is prefixed with one of staging,stg,preprod,prod,sandbox,sbox string. Means that when you refresh your preprod, staging environments from production, you have to rename the target schema from prod_ to stg_ or something like. It is possible, but it is never easy.", + fixes: &[ + "Keep the same schema name across environments. Prefer prefix or suffix the database name", + ], + }), + "S001" => Some(RuleMetadata { + code: "S001", + name: "schemaWithDefaultRoleNotGranted", + scope: "SCHEMA", + description: "The schema has no default role. Means that futur table will not be granted through a role. So you will have to re-execute grants on it.", + fixes: &[ + "add a default privilege=> ALTER DEFAULT PRIVILEGES IN SCHEMA for user ", + ], + }), + "B011" => Some(RuleMetadata { + code: "B011", + name: "severalTableOwnerInSchema", + scope: "BASE", + description: "In a schema there are several tables owned by different owners.", + fixes: &["change table owners to the same functional role"], + }), + "S003" => Some(RuleMetadata { + code: "S003", + name: "unsecuredPublicSchema", + scope: "SCHEMA", + description: "Only authorized users should be allowed to create objects.", + fixes: &["REVOKE CREATE ON SCHEMA FROM PUBLIC"], + }), + _ => None, + } +} diff --git a/crates/pgls_pglinter/src/rule.rs b/crates/pgls_pglinter/src/rule.rs new file mode 100644 index 000000000..43941a770 --- /dev/null +++ b/crates/pgls_pglinter/src/rule.rs @@ -0,0 +1,21 @@ +//! Generated file, do not edit by hand, see `xtask/codegen` + +#![doc = r" Generated file, do not edit by hand, see `xtask/codegen`"] +use pgls_analyse::RuleMeta; +#[doc = r" Trait for pglinter (database-level) rules"] +#[doc = r""] +#[doc = r" Pglinter rules are different from linter rules:"] +#[doc = r" - They execute SQL queries against the database via pglinter extension"] +#[doc = r" - They don't have AST-based execution"] +#[doc = r" - Rule logic is in the pglinter Postgres extension"] +#[doc = r" - Threshold configuration (warning/error levels) is handled by pglinter extension"] +pub trait PglinterRule: RuleMeta { + #[doc = r#" Rule code (e.g., "B001", "S001", "C001")"#] + const CODE: &'static str; + #[doc = r" Rule scope (BASE, SCHEMA, or CLUSTER)"] + const SCOPE: &'static str; + #[doc = r" Description of what the rule detects"] + const DESCRIPTION: &'static str; + #[doc = r" Suggested fixes for violations"] + const FIXES: &'static [&'static str]; +} diff --git a/crates/pgls_pglinter/src/rules/base/composite_primary_key_too_many_columns.rs b/crates/pgls_pglinter/src/rules/base/composite_primary_key_too_many_columns.rs new file mode 100644 index 000000000..7a04cf2be --- /dev/null +++ b/crates/pgls_pglinter/src/rules/base/composite_primary_key_too_many_columns.rs @@ -0,0 +1,15 @@ +//! Generated file, do not edit by hand, see `xtask/codegen` + +#![doc = r" Generated file, do not edit by hand, see `xtask/codegen`"] +use crate::rule::PglinterRule; +::pgls_analyse::declare_rule! { # [doc = "# CompositePrimaryKeyTooManyColumns (B012)\n\nDetect tables with composite primary keys involving more than 4 columns\n\n## Configuration\n\nEnable or disable this rule in your configuration:\n\n```json\n{\n \"pglinter\": {\n \"rules\": {\n \"base\": {\n \"compositePrimaryKeyTooManyColumns\": \"warn\"\n }\n }\n }\n}\n```\n\n## Thresholds\n\n- Warning level: 1%\n- Error level: 80%\n\n## Fixes\n\n- Consider redesigning the table to avoid composite primary keys with more than 4 columns\n- Use surrogate keys (e.g., serial, UUID) instead of composite primary keys, and establish unique constraints on necessary column combinations, to enforce uniqueness.\n\n## Documentation\n\nSee: "] pub CompositePrimaryKeyTooManyColumns { version : "1.0.0" , name : "compositePrimaryKeyTooManyColumns" , severity : pgls_diagnostics :: Severity :: Warning , recommended : true , } } +impl PglinterRule for CompositePrimaryKeyTooManyColumns { + const CODE: &'static str = "B012"; + const SCOPE: &'static str = "BASE"; + const DESCRIPTION: &'static str = + "Detect tables with composite primary keys involving more than 4 columns"; + const FIXES: &'static [&'static str] = &[ + "Consider redesigning the table to avoid composite primary keys with more than 4 columns", + "Use surrogate keys (e.g., serial, UUID) instead of composite primary keys, and establish unique constraints on necessary column combinations, to enforce uniqueness.", + ]; +} diff --git a/crates/pgls_pglinter/src/rules/base/how_many_objects_with_uppercase.rs b/crates/pgls_pglinter/src/rules/base/how_many_objects_with_uppercase.rs new file mode 100644 index 000000000..8210f0068 --- /dev/null +++ b/crates/pgls_pglinter/src/rules/base/how_many_objects_with_uppercase.rs @@ -0,0 +1,12 @@ +//! Generated file, do not edit by hand, see `xtask/codegen` + +#![doc = r" Generated file, do not edit by hand, see `xtask/codegen`"] +use crate::rule::PglinterRule; +::pgls_analyse::declare_rule! { # [doc = "# HowManyObjectsWithUppercase (B005)\n\nCount number of objects with uppercase in name or in columns.\n\n## Configuration\n\nEnable or disable this rule in your configuration:\n\n```json\n{\n \"pglinter\": {\n \"rules\": {\n \"base\": {\n \"howManyObjectsWithUppercase\": \"warn\"\n }\n }\n }\n}\n```\n\n## Thresholds\n\n- Warning level: 20%\n- Error level: 80%\n\n## Fixes\n\n- Do not use uppercase for any database objects\n\n## Documentation\n\nSee: "] pub HowManyObjectsWithUppercase { version : "1.0.0" , name : "howManyObjectsWithUppercase" , severity : pgls_diagnostics :: Severity :: Warning , recommended : true , } } +impl PglinterRule for HowManyObjectsWithUppercase { + const CODE: &'static str = "B005"; + const SCOPE: &'static str = "BASE"; + const DESCRIPTION: &'static str = + "Count number of objects with uppercase in name or in columns."; + const FIXES: &'static [&'static str] = &["Do not use uppercase for any database objects"]; +} diff --git a/crates/pgls_pglinter/src/rules/base/how_many_redudant_index.rs b/crates/pgls_pglinter/src/rules/base/how_many_redudant_index.rs new file mode 100644 index 000000000..82ce8de0b --- /dev/null +++ b/crates/pgls_pglinter/src/rules/base/how_many_redudant_index.rs @@ -0,0 +1,13 @@ +//! Generated file, do not edit by hand, see `xtask/codegen` + +#![doc = r" Generated file, do not edit by hand, see `xtask/codegen`"] +use crate::rule::PglinterRule; +::pgls_analyse::declare_rule! { # [doc = "# HowManyRedudantIndex (B002)\n\nCount number of redundant index vs nb index.\n\n## Configuration\n\nEnable or disable this rule in your configuration:\n\n```json\n{\n \"pglinter\": {\n \"rules\": {\n \"base\": {\n \"howManyRedudantIndex\": \"warn\"\n }\n }\n }\n}\n```\n\n## Thresholds\n\n- Warning level: 1%\n- Error level: 80%\n\n## Fixes\n\n- remove duplicated index or check if a constraint does not create a redundant index, or change warning/error threshold\n\n## Documentation\n\nSee: "] pub HowManyRedudantIndex { version : "1.0.0" , name : "howManyRedudantIndex" , severity : pgls_diagnostics :: Severity :: Warning , recommended : true , } } +impl PglinterRule for HowManyRedudantIndex { + const CODE: &'static str = "B002"; + const SCOPE: &'static str = "BASE"; + const DESCRIPTION: &'static str = "Count number of redundant index vs nb index."; + const FIXES: &'static [&'static str] = &[ + "remove duplicated index or check if a constraint does not create a redundant index, or change warning/error threshold", + ]; +} diff --git a/crates/pgls_pglinter/src/rules/base/how_many_table_without_index_on_fk.rs b/crates/pgls_pglinter/src/rules/base/how_many_table_without_index_on_fk.rs new file mode 100644 index 000000000..a737d529a --- /dev/null +++ b/crates/pgls_pglinter/src/rules/base/how_many_table_without_index_on_fk.rs @@ -0,0 +1,12 @@ +//! Generated file, do not edit by hand, see `xtask/codegen` + +#![doc = r" Generated file, do not edit by hand, see `xtask/codegen`"] +use crate::rule::PglinterRule; +::pgls_analyse::declare_rule! { # [doc = "# HowManyTableWithoutIndexOnFk (B003)\n\nCount number of tables without index on foreign key.\n\n## Configuration\n\nEnable or disable this rule in your configuration:\n\n```json\n{\n \"pglinter\": {\n \"rules\": {\n \"base\": {\n \"howManyTableWithoutIndexOnFk\": \"warn\"\n }\n }\n }\n}\n```\n\n## Thresholds\n\n- Warning level: 1%\n- Error level: 80%\n\n## Fixes\n\n- create a index on foreign key or change warning/error threshold\n\n## Documentation\n\nSee: "] pub HowManyTableWithoutIndexOnFk { version : "1.0.0" , name : "howManyTableWithoutIndexOnFk" , severity : pgls_diagnostics :: Severity :: Warning , recommended : true , } } +impl PglinterRule for HowManyTableWithoutIndexOnFk { + const CODE: &'static str = "B003"; + const SCOPE: &'static str = "BASE"; + const DESCRIPTION: &'static str = "Count number of tables without index on foreign key."; + const FIXES: &'static [&'static str] = + &["create a index on foreign key or change warning/error threshold"]; +} diff --git a/crates/pgls_pglinter/src/rules/base/how_many_table_without_primary_key.rs b/crates/pgls_pglinter/src/rules/base/how_many_table_without_primary_key.rs new file mode 100644 index 000000000..b708f7055 --- /dev/null +++ b/crates/pgls_pglinter/src/rules/base/how_many_table_without_primary_key.rs @@ -0,0 +1,12 @@ +//! Generated file, do not edit by hand, see `xtask/codegen` + +#![doc = r" Generated file, do not edit by hand, see `xtask/codegen`"] +use crate::rule::PglinterRule; +::pgls_analyse::declare_rule! { # [doc = "# HowManyTableWithoutPrimaryKey (B001)\n\nCount number of tables without primary key.\n\n## Configuration\n\nEnable or disable this rule in your configuration:\n\n```json\n{\n \"pglinter\": {\n \"rules\": {\n \"base\": {\n \"howManyTableWithoutPrimaryKey\": \"warn\"\n }\n }\n }\n}\n```\n\n## Thresholds\n\n- Warning level: 1%\n- Error level: 80%\n\n## Fixes\n\n- create a primary key or change warning/error threshold\n\n## Documentation\n\nSee: "] pub HowManyTableWithoutPrimaryKey { version : "1.0.0" , name : "howManyTableWithoutPrimaryKey" , severity : pgls_diagnostics :: Severity :: Warning , recommended : true , } } +impl PglinterRule for HowManyTableWithoutPrimaryKey { + const CODE: &'static str = "B001"; + const SCOPE: &'static str = "BASE"; + const DESCRIPTION: &'static str = "Count number of tables without primary key."; + const FIXES: &'static [&'static str] = + &["create a primary key or change warning/error threshold"]; +} diff --git a/crates/pgls_pglinter/src/rules/base/how_many_tables_never_selected.rs b/crates/pgls_pglinter/src/rules/base/how_many_tables_never_selected.rs new file mode 100644 index 000000000..4721485d6 --- /dev/null +++ b/crates/pgls_pglinter/src/rules/base/how_many_tables_never_selected.rs @@ -0,0 +1,12 @@ +//! Generated file, do not edit by hand, see `xtask/codegen` + +#![doc = r" Generated file, do not edit by hand, see `xtask/codegen`"] +use crate::rule::PglinterRule; +::pgls_analyse::declare_rule! { # [doc = "# HowManyTablesNeverSelected (B006)\n\nCount number of table(s) that has never been selected.\n\n## Configuration\n\nEnable or disable this rule in your configuration:\n\n```json\n{\n \"pglinter\": {\n \"rules\": {\n \"base\": {\n \"howManyTablesNeverSelected\": \"warn\"\n }\n }\n }\n}\n```\n\n## Thresholds\n\n- Warning level: 1%\n- Error level: 80%\n\n## Fixes\n\n- Is it necessary to update/delete/insert rows in table(s) that are never selected ?\n\n## Documentation\n\nSee: "] pub HowManyTablesNeverSelected { version : "1.0.0" , name : "howManyTablesNeverSelected" , severity : pgls_diagnostics :: Severity :: Warning , recommended : true , } } +impl PglinterRule for HowManyTablesNeverSelected { + const CODE: &'static str = "B006"; + const SCOPE: &'static str = "BASE"; + const DESCRIPTION: &'static str = "Count number of table(s) that has never been selected."; + const FIXES: &'static [&'static str] = + &["Is it necessary to update/delete/insert rows in table(s) that are never selected ?"]; +} diff --git a/crates/pgls_pglinter/src/rules/base/how_many_tables_with_fk_mismatch.rs b/crates/pgls_pglinter/src/rules/base/how_many_tables_with_fk_mismatch.rs new file mode 100644 index 000000000..3e887c1d4 --- /dev/null +++ b/crates/pgls_pglinter/src/rules/base/how_many_tables_with_fk_mismatch.rs @@ -0,0 +1,15 @@ +//! Generated file, do not edit by hand, see `xtask/codegen` + +#![doc = r" Generated file, do not edit by hand, see `xtask/codegen`"] +use crate::rule::PglinterRule; +::pgls_analyse::declare_rule! { # [doc = "# HowManyTablesWithFkMismatch (B008)\n\nCount number of tables with foreign keys that do not match the key reference type.\n\n## Configuration\n\nEnable or disable this rule in your configuration:\n\n```json\n{\n \"pglinter\": {\n \"rules\": {\n \"base\": {\n \"howManyTablesWithFkMismatch\": \"warn\"\n }\n }\n }\n}\n```\n\n## Thresholds\n\n- Warning level: 1%\n- Error level: 80%\n\n## Fixes\n\n- Consider column type adjustments to ensure foreign key matches referenced key type\n- ask a dba\n\n## Documentation\n\nSee: "] pub HowManyTablesWithFkMismatch { version : "1.0.0" , name : "howManyTablesWithFkMismatch" , severity : pgls_diagnostics :: Severity :: Warning , recommended : true , } } +impl PglinterRule for HowManyTablesWithFkMismatch { + const CODE: &'static str = "B008"; + const SCOPE: &'static str = "BASE"; + const DESCRIPTION: &'static str = + "Count number of tables with foreign keys that do not match the key reference type."; + const FIXES: &'static [&'static str] = &[ + "Consider column type adjustments to ensure foreign key matches referenced key type", + "ask a dba", + ]; +} diff --git a/crates/pgls_pglinter/src/rules/base/how_many_tables_with_fk_outside_schema.rs b/crates/pgls_pglinter/src/rules/base/how_many_tables_with_fk_outside_schema.rs new file mode 100644 index 000000000..f25318891 --- /dev/null +++ b/crates/pgls_pglinter/src/rules/base/how_many_tables_with_fk_outside_schema.rs @@ -0,0 +1,15 @@ +//! Generated file, do not edit by hand, see `xtask/codegen` + +#![doc = r" Generated file, do not edit by hand, see `xtask/codegen`"] +use crate::rule::PglinterRule; +::pgls_analyse::declare_rule! { # [doc = "# HowManyTablesWithFkOutsideSchema (B007)\n\nCount number of tables with foreign keys outside their schema.\n\n## Configuration\n\nEnable or disable this rule in your configuration:\n\n```json\n{\n \"pglinter\": {\n \"rules\": {\n \"base\": {\n \"howManyTablesWithFkOutsideSchema\": \"warn\"\n }\n }\n }\n}\n```\n\n## Thresholds\n\n- Warning level: 20%\n- Error level: 80%\n\n## Fixes\n\n- Consider restructuring schema design to keep related tables in same schema\n- ask a dba\n\n## Documentation\n\nSee: "] pub HowManyTablesWithFkOutsideSchema { version : "1.0.0" , name : "howManyTablesWithFkOutsideSchema" , severity : pgls_diagnostics :: Severity :: Warning , recommended : true , } } +impl PglinterRule for HowManyTablesWithFkOutsideSchema { + const CODE: &'static str = "B007"; + const SCOPE: &'static str = "BASE"; + const DESCRIPTION: &'static str = + "Count number of tables with foreign keys outside their schema."; + const FIXES: &'static [&'static str] = &[ + "Consider restructuring schema design to keep related tables in same schema", + "ask a dba", + ]; +} diff --git a/crates/pgls_pglinter/src/rules/base/how_many_tables_with_reserved_keywords.rs b/crates/pgls_pglinter/src/rules/base/how_many_tables_with_reserved_keywords.rs new file mode 100644 index 000000000..7b7efda8f --- /dev/null +++ b/crates/pgls_pglinter/src/rules/base/how_many_tables_with_reserved_keywords.rs @@ -0,0 +1,15 @@ +//! Generated file, do not edit by hand, see `xtask/codegen` + +#![doc = r" Generated file, do not edit by hand, see `xtask/codegen`"] +use crate::rule::PglinterRule; +::pgls_analyse::declare_rule! { # [doc = "# HowManyTablesWithReservedKeywords (B010)\n\nCount number of database objects using reserved keywords in their names.\n\n## Configuration\n\nEnable or disable this rule in your configuration:\n\n```json\n{\n \"pglinter\": {\n \"rules\": {\n \"base\": {\n \"howManyTablesWithReservedKeywords\": \"warn\"\n }\n }\n }\n}\n```\n\n## Thresholds\n\n- Warning level: 20%\n- Error level: 80%\n\n## Fixes\n\n- Rename database objects to avoid using reserved keywords.\n- Using reserved keywords can lead to SQL syntax errors and maintenance difficulties.\n\n## Documentation\n\nSee: "] pub HowManyTablesWithReservedKeywords { version : "1.0.0" , name : "howManyTablesWithReservedKeywords" , severity : pgls_diagnostics :: Severity :: Warning , recommended : true , } } +impl PglinterRule for HowManyTablesWithReservedKeywords { + const CODE: &'static str = "B010"; + const SCOPE: &'static str = "BASE"; + const DESCRIPTION: &'static str = + "Count number of database objects using reserved keywords in their names."; + const FIXES: &'static [&'static str] = &[ + "Rename database objects to avoid using reserved keywords.", + "Using reserved keywords can lead to SQL syntax errors and maintenance difficulties.", + ]; +} diff --git a/crates/pgls_pglinter/src/rules/base/how_many_tables_with_same_trigger.rs b/crates/pgls_pglinter/src/rules/base/how_many_tables_with_same_trigger.rs new file mode 100644 index 000000000..70cef3a36 --- /dev/null +++ b/crates/pgls_pglinter/src/rules/base/how_many_tables_with_same_trigger.rs @@ -0,0 +1,15 @@ +//! Generated file, do not edit by hand, see `xtask/codegen` + +#![doc = r" Generated file, do not edit by hand, see `xtask/codegen`"] +use crate::rule::PglinterRule; +::pgls_analyse::declare_rule! { # [doc = "# HowManyTablesWithSameTrigger (B009)\n\nCount number of tables using the same trigger vs nb table with their own triggers.\n\n## Configuration\n\nEnable or disable this rule in your configuration:\n\n```json\n{\n \"pglinter\": {\n \"rules\": {\n \"base\": {\n \"howManyTablesWithSameTrigger\": \"warn\"\n }\n }\n }\n}\n```\n\n## Thresholds\n\n- Warning level: 20%\n- Error level: 80%\n\n## Fixes\n\n- For more readability and other considerations use one trigger function per table.\n- Sharing the same trigger function add more complexity.\n\n## Documentation\n\nSee: "] pub HowManyTablesWithSameTrigger { version : "1.0.0" , name : "howManyTablesWithSameTrigger" , severity : pgls_diagnostics :: Severity :: Warning , recommended : true , } } +impl PglinterRule for HowManyTablesWithSameTrigger { + const CODE: &'static str = "B009"; + const SCOPE: &'static str = "BASE"; + const DESCRIPTION: &'static str = + "Count number of tables using the same trigger vs nb table with their own triggers."; + const FIXES: &'static [&'static str] = &[ + "For more readability and other considerations use one trigger function per table.", + "Sharing the same trigger function add more complexity.", + ]; +} diff --git a/crates/pgls_pglinter/src/rules/base/how_many_unused_index.rs b/crates/pgls_pglinter/src/rules/base/how_many_unused_index.rs new file mode 100644 index 000000000..805fb5a16 --- /dev/null +++ b/crates/pgls_pglinter/src/rules/base/how_many_unused_index.rs @@ -0,0 +1,12 @@ +//! Generated file, do not edit by hand, see `xtask/codegen` + +#![doc = r" Generated file, do not edit by hand, see `xtask/codegen`"] +use crate::rule::PglinterRule; +::pgls_analyse::declare_rule! { # [doc = "# HowManyUnusedIndex (B004)\n\nCount number of unused index vs nb index (base on pg_stat_user_indexes, indexes associated to unique constraints are discard.)\n\n## Configuration\n\nEnable or disable this rule in your configuration:\n\n```json\n{\n \"pglinter\": {\n \"rules\": {\n \"base\": {\n \"howManyUnusedIndex\": \"warn\"\n }\n }\n }\n}\n```\n\n## Thresholds\n\n- Warning level: 20%\n- Error level: 80%\n\n## Fixes\n\n- remove unused index or change warning/error threshold\n\n## Documentation\n\nSee: "] pub HowManyUnusedIndex { version : "1.0.0" , name : "howManyUnusedIndex" , severity : pgls_diagnostics :: Severity :: Warning , recommended : true , } } +impl PglinterRule for HowManyUnusedIndex { + const CODE: &'static str = "B004"; + const SCOPE: &'static str = "BASE"; + const DESCRIPTION: &'static str = "Count number of unused index vs nb index (base on pg_stat_user_indexes, indexes associated to unique constraints are discard.)"; + const FIXES: &'static [&'static str] = + &["remove unused index or change warning/error threshold"]; +} diff --git a/crates/pgls_pglinter/src/rules/base/mod.rs b/crates/pgls_pglinter/src/rules/base/mod.rs new file mode 100644 index 000000000..c51a6b08d --- /dev/null +++ b/crates/pgls_pglinter/src/rules/base/mod.rs @@ -0,0 +1,16 @@ +//! Generated file, do not edit by hand, see `xtask/codegen` + +#![doc = r" Generated file, do not edit by hand, see `xtask/codegen`"] +pub mod composite_primary_key_too_many_columns; +pub mod how_many_objects_with_uppercase; +pub mod how_many_redudant_index; +pub mod how_many_table_without_index_on_fk; +pub mod how_many_table_without_primary_key; +pub mod how_many_tables_never_selected; +pub mod how_many_tables_with_fk_mismatch; +pub mod how_many_tables_with_fk_outside_schema; +pub mod how_many_tables_with_reserved_keywords; +pub mod how_many_tables_with_same_trigger; +pub mod how_many_unused_index; +pub mod several_table_owner_in_schema; +::pgls_analyse::declare_lint_group! { pub Base { name : "base" , rules : [self :: composite_primary_key_too_many_columns :: CompositePrimaryKeyTooManyColumns , self :: how_many_objects_with_uppercase :: HowManyObjectsWithUppercase , self :: how_many_redudant_index :: HowManyRedudantIndex , self :: how_many_table_without_index_on_fk :: HowManyTableWithoutIndexOnFk , self :: how_many_table_without_primary_key :: HowManyTableWithoutPrimaryKey , self :: how_many_tables_never_selected :: HowManyTablesNeverSelected , self :: how_many_tables_with_fk_mismatch :: HowManyTablesWithFkMismatch , self :: how_many_tables_with_fk_outside_schema :: HowManyTablesWithFkOutsideSchema , self :: how_many_tables_with_reserved_keywords :: HowManyTablesWithReservedKeywords , self :: how_many_tables_with_same_trigger :: HowManyTablesWithSameTrigger , self :: how_many_unused_index :: HowManyUnusedIndex , self :: several_table_owner_in_schema :: SeveralTableOwnerInSchema ,] } } diff --git a/crates/pgls_pglinter/src/rules/base/several_table_owner_in_schema.rs b/crates/pgls_pglinter/src/rules/base/several_table_owner_in_schema.rs new file mode 100644 index 000000000..d42899411 --- /dev/null +++ b/crates/pgls_pglinter/src/rules/base/several_table_owner_in_schema.rs @@ -0,0 +1,12 @@ +//! Generated file, do not edit by hand, see `xtask/codegen` + +#![doc = r" Generated file, do not edit by hand, see `xtask/codegen`"] +use crate::rule::PglinterRule; +::pgls_analyse::declare_rule! { # [doc = "# SeveralTableOwnerInSchema (B011)\n\nIn a schema there are several tables owned by different owners.\n\n## Configuration\n\nEnable or disable this rule in your configuration:\n\n```json\n{\n \"pglinter\": {\n \"rules\": {\n \"base\": {\n \"severalTableOwnerInSchema\": \"warn\"\n }\n }\n }\n}\n```\n\n## Thresholds\n\n- Warning level: 1%\n- Error level: 80%\n\n## Fixes\n\n- change table owners to the same functional role\n\n## Documentation\n\nSee: "] pub SeveralTableOwnerInSchema { version : "1.0.0" , name : "severalTableOwnerInSchema" , severity : pgls_diagnostics :: Severity :: Warning , recommended : true , } } +impl PglinterRule for SeveralTableOwnerInSchema { + const CODE: &'static str = "B011"; + const SCOPE: &'static str = "BASE"; + const DESCRIPTION: &'static str = + "In a schema there are several tables owned by different owners."; + const FIXES: &'static [&'static str] = &["change table owners to the same functional role"]; +} diff --git a/crates/pgls_pglinter/src/rules/cluster/mod.rs b/crates/pgls_pglinter/src/rules/cluster/mod.rs new file mode 100644 index 000000000..7e948dd5c --- /dev/null +++ b/crates/pgls_pglinter/src/rules/cluster/mod.rs @@ -0,0 +1,7 @@ +//! Generated file, do not edit by hand, see `xtask/codegen` + +#![doc = r" Generated file, do not edit by hand, see `xtask/codegen`"] +pub mod password_encryption_is_md5; +pub mod pg_hba_entries_with_method_trust_or_password_should_not_exists; +pub mod pg_hba_entries_with_method_trust_should_not_exists; +::pgls_analyse::declare_lint_group! { pub Cluster { name : "cluster" , rules : [self :: password_encryption_is_md5 :: PasswordEncryptionIsMd5 , self :: pg_hba_entries_with_method_trust_or_password_should_not_exists :: PgHbaEntriesWithMethodTrustOrPasswordShouldNotExists , self :: pg_hba_entries_with_method_trust_should_not_exists :: PgHbaEntriesWithMethodTrustShouldNotExists ,] } } diff --git a/crates/pgls_pglinter/src/rules/cluster/password_encryption_is_md5.rs b/crates/pgls_pglinter/src/rules/cluster/password_encryption_is_md5.rs new file mode 100644 index 000000000..7dad19a17 --- /dev/null +++ b/crates/pgls_pglinter/src/rules/cluster/password_encryption_is_md5.rs @@ -0,0 +1,15 @@ +//! Generated file, do not edit by hand, see `xtask/codegen` + +#![doc = r" Generated file, do not edit by hand, see `xtask/codegen`"] +use crate::rule::PglinterRule; +::pgls_analyse::declare_rule! { # [doc = "# PasswordEncryptionIsMd5 (C003)\n\nThis configuration is not secure anymore and will prevent an upgrade to Postgres 18. Warning, you will need to reset all passwords after this is changed to scram-sha-256.\n\n## Configuration\n\nEnable or disable this rule in your configuration:\n\n```json\n{\n \"pglinter\": {\n \"rules\": {\n \"cluster\": {\n \"passwordEncryptionIsMd5\": \"warn\"\n }\n }\n }\n}\n```\n\n## Thresholds\n\n- Warning level: 20%\n- Error level: 80%\n\n## Fixes\n\n- change password_encryption parameter to scram-sha-256 (ALTER SYSTEM SET password_encryption = \n- scram-sha-256\n- ). Warning, you will need to reset all passwords after this parameter is updated.\n\n## Documentation\n\nSee: "] pub PasswordEncryptionIsMd5 { version : "1.0.0" , name : "passwordEncryptionIsMd5" , severity : pgls_diagnostics :: Severity :: Warning , recommended : true , } } +impl PglinterRule for PasswordEncryptionIsMd5 { + const CODE: &'static str = "C003"; + const SCOPE: &'static str = "CLUSTER"; + const DESCRIPTION: &'static str = "This configuration is not secure anymore and will prevent an upgrade to Postgres 18. Warning, you will need to reset all passwords after this is changed to scram-sha-256."; + const FIXES: &'static [&'static str] = &[ + "change password_encryption parameter to scram-sha-256 (ALTER SYSTEM SET password_encryption = ", + "scram-sha-256", + " ). Warning, you will need to reset all passwords after this parameter is updated.", + ]; +} diff --git a/crates/pgls_pglinter/src/rules/cluster/pg_hba_entries_with_method_trust_or_password_should_not_exists.rs b/crates/pgls_pglinter/src/rules/cluster/pg_hba_entries_with_method_trust_or_password_should_not_exists.rs new file mode 100644 index 000000000..8cbb7e5d6 --- /dev/null +++ b/crates/pgls_pglinter/src/rules/cluster/pg_hba_entries_with_method_trust_or_password_should_not_exists.rs @@ -0,0 +1,11 @@ +//! Generated file, do not edit by hand, see `xtask/codegen` + +#![doc = r" Generated file, do not edit by hand, see `xtask/codegen`"] +use crate::rule::PglinterRule; +::pgls_analyse::declare_rule! { # [doc = "# PgHbaEntriesWithMethodTrustOrPasswordShouldNotExists (C002)\n\nThis configuration is extremely insecure and should only be used in a controlled, non-production environment for testing purposes. In a production environment, you should use more secure authentication methods such as md5, scram-sha-256, or cert, and restrict access to trusted IP addresses only.\n\n## Configuration\n\nEnable or disable this rule in your configuration:\n\n```json\n{\n \"pglinter\": {\n \"rules\": {\n \"cluster\": {\n \"pgHbaEntriesWithMethodTrustOrPasswordShouldNotExists\": \"warn\"\n }\n }\n }\n}\n```\n\n## Thresholds\n\n- Warning level: 20%\n- Error level: 80%\n\n## Fixes\n\n- change trust or password method in pg_hba.conf\n\n## Documentation\n\nSee: "] pub PgHbaEntriesWithMethodTrustOrPasswordShouldNotExists { version : "1.0.0" , name : "pgHbaEntriesWithMethodTrustOrPasswordShouldNotExists" , severity : pgls_diagnostics :: Severity :: Warning , recommended : true , } } +impl PglinterRule for PgHbaEntriesWithMethodTrustOrPasswordShouldNotExists { + const CODE: &'static str = "C002"; + const SCOPE: &'static str = "CLUSTER"; + const DESCRIPTION: &'static str = "This configuration is extremely insecure and should only be used in a controlled, non-production environment for testing purposes. In a production environment, you should use more secure authentication methods such as md5, scram-sha-256, or cert, and restrict access to trusted IP addresses only."; + const FIXES: &'static [&'static str] = &["change trust or password method in pg_hba.conf"]; +} diff --git a/crates/pgls_pglinter/src/rules/cluster/pg_hba_entries_with_method_trust_should_not_exists.rs b/crates/pgls_pglinter/src/rules/cluster/pg_hba_entries_with_method_trust_should_not_exists.rs new file mode 100644 index 000000000..096ae3bbc --- /dev/null +++ b/crates/pgls_pglinter/src/rules/cluster/pg_hba_entries_with_method_trust_should_not_exists.rs @@ -0,0 +1,11 @@ +//! Generated file, do not edit by hand, see `xtask/codegen` + +#![doc = r" Generated file, do not edit by hand, see `xtask/codegen`"] +use crate::rule::PglinterRule; +::pgls_analyse::declare_rule! { # [doc = "# PgHbaEntriesWithMethodTrustShouldNotExists (C001)\n\nThis configuration is extremely insecure and should only be used in a controlled, non-production environment for testing purposes. In a production environment, you should use more secure authentication methods such as md5, scram-sha-256, or cert, and restrict access to trusted IP addresses only.\n\n## Configuration\n\nEnable or disable this rule in your configuration:\n\n```json\n{\n \"pglinter\": {\n \"rules\": {\n \"cluster\": {\n \"pgHbaEntriesWithMethodTrustShouldNotExists\": \"warn\"\n }\n }\n }\n}\n```\n\n## Thresholds\n\n- Warning level: 20%\n- Error level: 80%\n\n## Fixes\n\n- change trust method in pg_hba.conf\n\n## Documentation\n\nSee: "] pub PgHbaEntriesWithMethodTrustShouldNotExists { version : "1.0.0" , name : "pgHbaEntriesWithMethodTrustShouldNotExists" , severity : pgls_diagnostics :: Severity :: Warning , recommended : true , } } +impl PglinterRule for PgHbaEntriesWithMethodTrustShouldNotExists { + const CODE: &'static str = "C001"; + const SCOPE: &'static str = "CLUSTER"; + const DESCRIPTION: &'static str = "This configuration is extremely insecure and should only be used in a controlled, non-production environment for testing purposes. In a production environment, you should use more secure authentication methods such as md5, scram-sha-256, or cert, and restrict access to trusted IP addresses only."; + const FIXES: &'static [&'static str] = &["change trust method in pg_hba.conf"]; +} diff --git a/crates/pgls_pglinter/src/rules/mod.rs b/crates/pgls_pglinter/src/rules/mod.rs new file mode 100644 index 000000000..428719e86 --- /dev/null +++ b/crates/pgls_pglinter/src/rules/mod.rs @@ -0,0 +1,7 @@ +//! Generated file, do not edit by hand, see `xtask/codegen` + +#![doc = r" Generated file, do not edit by hand, see `xtask/codegen`"] +pub mod base; +pub mod cluster; +pub mod schema; +::pgls_analyse::declare_category! { pub PgLinter { kind : Lint , groups : [self :: base :: Base , self :: cluster :: Cluster , self :: schema :: Schema ,] } } diff --git a/crates/pgls_pglinter/src/rules/schema/mod.rs b/crates/pgls_pglinter/src/rules/schema/mod.rs new file mode 100644 index 000000000..772dfa25c --- /dev/null +++ b/crates/pgls_pglinter/src/rules/schema/mod.rs @@ -0,0 +1,9 @@ +//! Generated file, do not edit by hand, see `xtask/codegen` + +#![doc = r" Generated file, do not edit by hand, see `xtask/codegen`"] +pub mod owner_schema_is_internal_role; +pub mod schema_owner_do_not_match_table_owner; +pub mod schema_prefixed_or_suffixed_with_envt; +pub mod schema_with_default_role_not_granted; +pub mod unsecured_public_schema; +::pgls_analyse::declare_lint_group! { pub Schema { name : "schema" , rules : [self :: owner_schema_is_internal_role :: OwnerSchemaIsInternalRole , self :: schema_owner_do_not_match_table_owner :: SchemaOwnerDoNotMatchTableOwner , self :: schema_prefixed_or_suffixed_with_envt :: SchemaPrefixedOrSuffixedWithEnvt , self :: schema_with_default_role_not_granted :: SchemaWithDefaultRoleNotGranted , self :: unsecured_public_schema :: UnsecuredPublicSchema ,] } } diff --git a/crates/pgls_pglinter/src/rules/schema/owner_schema_is_internal_role.rs b/crates/pgls_pglinter/src/rules/schema/owner_schema_is_internal_role.rs new file mode 100644 index 000000000..7abbc1d89 --- /dev/null +++ b/crates/pgls_pglinter/src/rules/schema/owner_schema_is_internal_role.rs @@ -0,0 +1,11 @@ +//! Generated file, do not edit by hand, see `xtask/codegen` + +#![doc = r" Generated file, do not edit by hand, see `xtask/codegen`"] +use crate::rule::PglinterRule; +::pgls_analyse::declare_rule! { # [doc = "# OwnerSchemaIsInternalRole (S004)\n\nOwner of schema should not be any internal pg roles, or owner is a superuser (not sure it is necesary).\n\n## Configuration\n\nEnable or disable this rule in your configuration:\n\n```json\n{\n \"pglinter\": {\n \"rules\": {\n \"schema\": {\n \"ownerSchemaIsInternalRole\": \"warn\"\n }\n }\n }\n}\n```\n\n## Thresholds\n\n- Warning level: 20%\n- Error level: 80%\n\n## Fixes\n\n- change schema owner to a functional role\n\n## Documentation\n\nSee: "] pub OwnerSchemaIsInternalRole { version : "1.0.0" , name : "ownerSchemaIsInternalRole" , severity : pgls_diagnostics :: Severity :: Warning , recommended : true , } } +impl PglinterRule for OwnerSchemaIsInternalRole { + const CODE: &'static str = "S004"; + const SCOPE: &'static str = "SCHEMA"; + const DESCRIPTION: &'static str = "Owner of schema should not be any internal pg roles, or owner is a superuser (not sure it is necesary)."; + const FIXES: &'static [&'static str] = &["change schema owner to a functional role"]; +} diff --git a/crates/pgls_pglinter/src/rules/schema/schema_owner_do_not_match_table_owner.rs b/crates/pgls_pglinter/src/rules/schema/schema_owner_do_not_match_table_owner.rs new file mode 100644 index 000000000..8072bfa1d --- /dev/null +++ b/crates/pgls_pglinter/src/rules/schema/schema_owner_do_not_match_table_owner.rs @@ -0,0 +1,12 @@ +//! Generated file, do not edit by hand, see `xtask/codegen` + +#![doc = r" Generated file, do not edit by hand, see `xtask/codegen`"] +use crate::rule::PglinterRule; +::pgls_analyse::declare_rule! { # [doc = "# SchemaOwnerDoNotMatchTableOwner (S005)\n\nThe schema owner and tables in the schema do not match.\n\n## Configuration\n\nEnable or disable this rule in your configuration:\n\n```json\n{\n \"pglinter\": {\n \"rules\": {\n \"schema\": {\n \"schemaOwnerDoNotMatchTableOwner\": \"warn\"\n }\n }\n }\n}\n```\n\n## Thresholds\n\n- Warning level: 20%\n- Error level: 80%\n\n## Fixes\n\n- For maintenance facilities, schema and tables owners should be the same.\n\n## Documentation\n\nSee: "] pub SchemaOwnerDoNotMatchTableOwner { version : "1.0.0" , name : "schemaOwnerDoNotMatchTableOwner" , severity : pgls_diagnostics :: Severity :: Warning , recommended : true , } } +impl PglinterRule for SchemaOwnerDoNotMatchTableOwner { + const CODE: &'static str = "S005"; + const SCOPE: &'static str = "SCHEMA"; + const DESCRIPTION: &'static str = "The schema owner and tables in the schema do not match."; + const FIXES: &'static [&'static str] = + &["For maintenance facilities, schema and tables owners should be the same."]; +} diff --git a/crates/pgls_pglinter/src/rules/schema/schema_prefixed_or_suffixed_with_envt.rs b/crates/pgls_pglinter/src/rules/schema/schema_prefixed_or_suffixed_with_envt.rs new file mode 100644 index 000000000..4c6ad66a2 --- /dev/null +++ b/crates/pgls_pglinter/src/rules/schema/schema_prefixed_or_suffixed_with_envt.rs @@ -0,0 +1,13 @@ +//! Generated file, do not edit by hand, see `xtask/codegen` + +#![doc = r" Generated file, do not edit by hand, see `xtask/codegen`"] +use crate::rule::PglinterRule; +::pgls_analyse::declare_rule! { # [doc = "# SchemaPrefixedOrSuffixedWithEnvt (S002)\n\nThe schema is prefixed with one of staging,stg,preprod,prod,sandbox,sbox string. Means that when you refresh your preprod, staging environments from production, you have to rename the target schema from prod_ to stg_ or something like. It is possible, but it is never easy.\n\n## Configuration\n\nEnable or disable this rule in your configuration:\n\n```json\n{\n \"pglinter\": {\n \"rules\": {\n \"schema\": {\n \"schemaPrefixedOrSuffixedWithEnvt\": \"warn\"\n }\n }\n }\n}\n```\n\n## Thresholds\n\n- Warning level: 1%\n- Error level: 1%\n\n## Fixes\n\n- Keep the same schema name across environments. Prefer prefix or suffix the database name\n\n## Documentation\n\nSee: "] pub SchemaPrefixedOrSuffixedWithEnvt { version : "1.0.0" , name : "schemaPrefixedOrSuffixedWithEnvt" , severity : pgls_diagnostics :: Severity :: Warning , recommended : true , } } +impl PglinterRule for SchemaPrefixedOrSuffixedWithEnvt { + const CODE: &'static str = "S002"; + const SCOPE: &'static str = "SCHEMA"; + const DESCRIPTION: &'static str = "The schema is prefixed with one of staging,stg,preprod,prod,sandbox,sbox string. Means that when you refresh your preprod, staging environments from production, you have to rename the target schema from prod_ to stg_ or something like. It is possible, but it is never easy."; + const FIXES: &'static [&'static str] = &[ + "Keep the same schema name across environments. Prefer prefix or suffix the database name", + ]; +} diff --git a/crates/pgls_pglinter/src/rules/schema/schema_with_default_role_not_granted.rs b/crates/pgls_pglinter/src/rules/schema/schema_with_default_role_not_granted.rs new file mode 100644 index 000000000..eb7c221a8 --- /dev/null +++ b/crates/pgls_pglinter/src/rules/schema/schema_with_default_role_not_granted.rs @@ -0,0 +1,14 @@ +//! Generated file, do not edit by hand, see `xtask/codegen` + +#![doc = r" Generated file, do not edit by hand, see `xtask/codegen`"] +use crate::rule::PglinterRule; +::pgls_analyse::declare_rule! { # [doc = "# SchemaWithDefaultRoleNotGranted (S001)\n\nThe schema has no default role. Means that futur table will not be granted through a role. So you will have to re-execute grants on it.\n\n## Configuration\n\nEnable or disable this rule in your configuration:\n\n```json\n{\n \"pglinter\": {\n \"rules\": {\n \"schema\": {\n \"schemaWithDefaultRoleNotGranted\": \"warn\"\n }\n }\n }\n}\n```\n\n## Thresholds\n\n- Warning level: 1%\n- Error level: 1%\n\n## Fixes\n\n- add a default privilege=> ALTER DEFAULT PRIVILEGES IN SCHEMA for user \n\n## Documentation\n\nSee: "] pub SchemaWithDefaultRoleNotGranted { version : "1.0.0" , name : "schemaWithDefaultRoleNotGranted" , severity : pgls_diagnostics :: Severity :: Warning , recommended : true , } } +impl PglinterRule for SchemaWithDefaultRoleNotGranted { + const CODE: &'static str = "S001"; + const SCOPE: &'static str = "SCHEMA"; + const DESCRIPTION: &'static str = "The schema has no default role. Means that futur table will not be granted through a role. So you will have to re-execute grants on it."; + const FIXES: &'static [&'static str] = &[ + "add a default privilege=> ALTER DEFAULT PRIVILEGES IN SCHEMA for user ", + ]; +} diff --git a/crates/pgls_pglinter/src/rules/schema/unsecured_public_schema.rs b/crates/pgls_pglinter/src/rules/schema/unsecured_public_schema.rs new file mode 100644 index 000000000..b71069ff6 --- /dev/null +++ b/crates/pgls_pglinter/src/rules/schema/unsecured_public_schema.rs @@ -0,0 +1,11 @@ +//! Generated file, do not edit by hand, see `xtask/codegen` + +#![doc = r" Generated file, do not edit by hand, see `xtask/codegen`"] +use crate::rule::PglinterRule; +::pgls_analyse::declare_rule! { # [doc = "# UnsecuredPublicSchema (S003)\n\nOnly authorized users should be allowed to create objects.\n\n## Configuration\n\nEnable or disable this rule in your configuration:\n\n```json\n{\n \"pglinter\": {\n \"rules\": {\n \"schema\": {\n \"unsecuredPublicSchema\": \"warn\"\n }\n }\n }\n}\n```\n\n## Thresholds\n\n- Warning level: 1%\n- Error level: 80%\n\n## Fixes\n\n- REVOKE CREATE ON SCHEMA FROM PUBLIC\n\n## Documentation\n\nSee: "] pub UnsecuredPublicSchema { version : "1.0.0" , name : "unsecuredPublicSchema" , severity : pgls_diagnostics :: Severity :: Warning , recommended : true , } } +impl PglinterRule for UnsecuredPublicSchema { + const CODE: &'static str = "S003"; + const SCOPE: &'static str = "SCHEMA"; + const DESCRIPTION: &'static str = "Only authorized users should be allowed to create objects."; + const FIXES: &'static [&'static str] = &["REVOKE CREATE ON SCHEMA FROM PUBLIC"]; +} diff --git a/crates/pgls_pglinter/src/sarif.rs b/crates/pgls_pglinter/src/sarif.rs new file mode 100644 index 000000000..d57d0979f --- /dev/null +++ b/crates/pgls_pglinter/src/sarif.rs @@ -0,0 +1,172 @@ +//! Generic SARIF (Static Analysis Results Interchange Format) parser +//! +//! SARIF is a standard format for static analysis tool output. +//! See: https://sarifweb.azurewebsites.net/ + +use serde::Deserialize; + +/// SARIF 2.1.0 root object +#[derive(Debug, Deserialize)] +pub struct SarifLog { + #[serde(default)] + pub runs: Vec, +} + +/// A single run of a static analysis tool +#[derive(Debug, Deserialize)] +pub struct Run { + #[serde(default)] + pub results: Vec, + pub tool: Option, +} + +/// Information about the tool that produced the results +#[derive(Debug, Deserialize)] +pub struct Tool { + pub driver: Option, +} + +/// The tool driver (main component) +#[derive(Debug, Deserialize)] +pub struct Driver { + pub name: Option, + pub version: Option, +} + +/// A single result from the analysis +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Result { + /// The rule ID that was violated + pub rule_id: Option, + /// Severity level: "error", "warning", "note", "none" + pub level: Option, + /// The result message + pub message: Option, + /// Locations where the issue was found + #[serde(default)] + pub locations: Vec, +} + +/// A message with text content +#[derive(Debug, Deserialize)] +pub struct Message { + pub text: Option, +} + +/// A location in the source +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Location { + pub physical_location: Option, + pub logical_locations: Option>, +} + +/// A physical location (file, line, column) +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct PhysicalLocation { + pub artifact_location: Option, + pub region: Option, +} + +/// Location of an artifact (file) +#[derive(Debug, Deserialize)] +pub struct ArtifactLocation { + pub uri: Option, +} + +/// A region within a file +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Region { + pub start_line: Option, + pub start_column: Option, + pub end_line: Option, + pub end_column: Option, +} + +/// A logical location (schema, table, function name, etc.) +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct LogicalLocation { + pub name: Option, + pub fully_qualified_name: Option, + pub kind: Option, +} + +impl SarifLog { + /// Parse SARIF JSON into a structured log + pub fn parse(json: &str) -> std::result::Result { + serde_json::from_str(json) + } + + /// Get all results from all runs + pub fn all_results(&self) -> impl Iterator { + self.runs.iter().flat_map(|run| run.results.iter()) + } + + /// Check if there are any results + pub fn has_results(&self) -> bool { + self.runs.iter().any(|run| !run.results.is_empty()) + } +} + +impl Result { + /// Get the severity level, defaulting to "warning" + pub fn level_str(&self) -> &str { + self.level.as_deref().unwrap_or("warning") + } + + /// Get the message text, defaulting to empty string + pub fn message_text(&self) -> &str { + self.message + .as_ref() + .and_then(|m| m.text.as_deref()) + .unwrap_or("") + } + + /// Get logical location names (e.g., affected database objects) + pub fn logical_location_names(&self) -> Vec<&str> { + self.locations + .iter() + .filter_map(|loc| loc.logical_locations.as_ref()) + .flatten() + .filter_map(|ll| ll.fully_qualified_name.as_deref().or(ll.name.as_deref())) + .collect() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_parse_minimal_sarif() { + let json = r#"{ + "runs": [{ + "results": [{ + "ruleId": "B001", + "level": "warning", + "message": { "text": "Table without primary key" } + }] + }] + }"#; + + let log = SarifLog::parse(json).unwrap(); + assert!(log.has_results()); + + let results: Vec<_> = log.all_results().collect(); + assert_eq!(results.len(), 1); + assert_eq!(results[0].rule_id.as_deref(), Some("B001")); + assert_eq!(results[0].level_str(), "warning"); + assert_eq!(results[0].message_text(), "Table without primary key"); + } + + #[test] + fn test_parse_empty_sarif() { + let json = r#"{"runs": [{"results": []}]}"#; + let log = SarifLog::parse(json).unwrap(); + assert!(!log.has_results()); + } +} diff --git a/crates/pgls_pglinter/tests/diagnostics.rs b/crates/pgls_pglinter/tests/diagnostics.rs new file mode 100644 index 000000000..abdd68bcd --- /dev/null +++ b/crates/pgls_pglinter/tests/diagnostics.rs @@ -0,0 +1,267 @@ +//! Integration tests for pglinter diagnostics +//! +//! These tests require the pglinter extension to be installed in the test database. + +use pgls_analyse::AnalysisFilter; +use pgls_console::fmt::{Formatter, HTML}; +use pgls_diagnostics::{Diagnostic, LogCategory, Visit}; +use pgls_pglinter::{PglinterCache, PglinterParams, run_pglinter}; +use pgls_schema_cache::SchemaCache; +use sqlx::PgPool; +use std::fmt::Write; +use std::io; + +struct TestVisitor { + logs: Vec, +} + +impl TestVisitor { + fn new() -> Self { + Self { logs: Vec::new() } + } + + fn into_string(self) -> String { + self.logs.join("\n") + } +} + +impl Visit for TestVisitor { + fn record_log( + &mut self, + category: LogCategory, + text: &dyn pgls_console::fmt::Display, + ) -> io::Result<()> { + let prefix = match category { + LogCategory::None => "", + LogCategory::Info => "[Info] ", + LogCategory::Warn => "[Warn] ", + LogCategory::Error => "[Error] ", + }; + + let mut buffer = vec![]; + let mut writer = HTML::new(&mut buffer); + let mut formatter = Formatter::new(&mut writer); + text.fmt(&mut formatter)?; + + let text_str = String::from_utf8(buffer).unwrap(); + self.logs.push(format!("{prefix}{text_str}")); + Ok(()) + } +} + +struct TestSetup<'a> { + name: &'a str, + setup: &'a str, + test_db: &'a PgPool, +} + +impl TestSetup<'_> { + async fn test(self) { + // Load schema cache + let schema_cache = SchemaCache::load(self.test_db) + .await + .expect("Failed to load schema cache"); + + // Assert pglinter extension is installed + assert!( + schema_cache.extensions.iter().any(|e| e.name == "pglinter"), + "pglinter extension must be installed for tests to run" + ); + + // Run setup SQL + sqlx::raw_sql(self.setup) + .execute(self.test_db) + .await + .expect("Failed to setup test database"); + + // Reload schema cache after setup + let schema_cache = SchemaCache::load(self.test_db) + .await + .expect("Failed to reload schema cache"); + + // Load pglinter cache + let cache = PglinterCache::load(self.test_db, &schema_cache) + .await + .expect("Failed to load pglinter cache"); + + // Run pglinter checks with all rules enabled + let filter = AnalysisFilter::default(); + let diagnostics = run_pglinter( + PglinterParams { + conn: self.test_db, + schema_cache: &schema_cache, + }, + &filter, + Some(&cache), + ) + .await + .expect("Failed to run pglinter checks"); + + let content = if diagnostics.is_empty() { + String::from("No Diagnostics") + } else { + let mut result = String::new(); + + for (idx, diagnostic) in diagnostics.iter().enumerate() { + if idx > 0 { + writeln!(&mut result).unwrap(); + writeln!(&mut result, "---").unwrap(); + writeln!(&mut result).unwrap(); + } + + // Write category + let category_name = diagnostic.category().map(|c| c.name()).unwrap_or("unknown"); + writeln!(&mut result, "Category: {category_name}").unwrap(); + + // Write severity + writeln!(&mut result, "Severity: {:?}", diagnostic.severity()).unwrap(); + + // Write message + let mut msg_content = vec![]; + let mut writer = HTML::new(&mut msg_content); + let mut formatter = Formatter::new(&mut writer); + diagnostic.message(&mut formatter).unwrap(); + writeln!( + &mut result, + "Message: {}", + String::from_utf8(msg_content).unwrap() + ) + .unwrap(); + + // Write advices using custom visitor + let mut visitor = TestVisitor::new(); + diagnostic.advices(&mut visitor).unwrap(); + let advice_text = visitor.into_string(); + if !advice_text.is_empty() { + writeln!(&mut result, "Advices:\n{advice_text}").unwrap(); + } + } + + result + }; + + insta::with_settings!({ + prepend_module_to_snapshot => false, + }, { + insta::assert_snapshot!(self.name, content); + }); + } +} + +/// Test that checks extension availability +#[sqlx::test(migrator = "pgls_test_utils::MIGRATIONS")] +async fn extension_check(test_db: PgPool) { + let schema_cache = SchemaCache::load(&test_db) + .await + .expect("Failed to load schema cache"); + + assert!( + schema_cache.extensions.iter().any(|e| e.name == "pglinter"), + "pglinter extension must be installed for tests to run" + ); +} + +/// Test B001: Table without primary key +#[sqlx::test(migrator = "pgls_test_utils::MIGRATIONS")] +async fn table_without_primary_key(test_db: PgPool) { + TestSetup { + name: "table_without_primary_key", + setup: r#" + CREATE TABLE public.test_no_pk ( + name text, + value integer + ); + "#, + test_db: &test_db, + } + .test() + .await; +} + +/// Test with a clean table (has primary key) +#[sqlx::test(migrator = "pgls_test_utils::MIGRATIONS")] +async fn table_with_primary_key(test_db: PgPool) { + TestSetup { + name: "table_with_primary_key", + setup: r#" + CREATE TABLE public.test_with_pk ( + id serial PRIMARY KEY, + name text + ); + "#, + test_db: &test_db, + } + .test() + .await; +} + +/// Test B005: Objects with uppercase names +#[sqlx::test(migrator = "pgls_test_utils::MIGRATIONS")] +async fn objects_with_uppercase(test_db: PgPool) { + TestSetup { + name: "objects_with_uppercase", + setup: r#" + CREATE TABLE public."TestTable" ( + id serial PRIMARY KEY, + "UserName" text + ); + "#, + test_db: &test_db, + } + .test() + .await; +} + +/// Test B003: Foreign key without index +#[sqlx::test(migrator = "pgls_test_utils::MIGRATIONS")] +async fn fk_without_index(test_db: PgPool) { + TestSetup { + name: "fk_without_index", + setup: r#" + CREATE TABLE public.parent_table ( + id serial PRIMARY KEY, + name text + ); + + CREATE TABLE public.child_table ( + id serial PRIMARY KEY, + parent_id integer NOT NULL REFERENCES public.parent_table(id) + ); + "#, + test_db: &test_db, + } + .test() + .await; +} + +/// Test multiple issues at once +#[sqlx::test(migrator = "pgls_test_utils::MIGRATIONS")] +async fn multiple_issues(test_db: PgPool) { + TestSetup { + name: "multiple_issues", + setup: r#" + -- Table without primary key + CREATE TABLE public.no_pk ( + name text + ); + + -- Table with uppercase name + CREATE TABLE public."BadName" ( + id serial PRIMARY KEY + ); + + -- FK without index + CREATE TABLE public.ref_parent ( + id serial PRIMARY KEY + ); + + CREATE TABLE public.ref_child ( + id serial PRIMARY KEY, + parent_id integer REFERENCES public.ref_parent(id) + ); + "#, + test_db: &test_db, + } + .test() + .await; +} diff --git a/crates/pgls_workspace/src/settings.rs b/crates/pgls_workspace/src/settings.rs index db9cfc173..c798e54ae 100644 --- a/crates/pgls_workspace/src/settings.rs +++ b/crates/pgls_workspace/src/settings.rs @@ -20,6 +20,7 @@ use pgls_configuration::{ files::FilesConfiguration, format::{FormatConfiguration, IndentStyle, KeywordCase}, migrations::{MigrationsConfiguration, PartialMigrationsConfiguration}, + pglinter::PglinterConfiguration, plpgsql_check::PlPgSqlCheckConfiguration, splinter::SplinterConfiguration, }; @@ -223,6 +224,9 @@ pub struct Settings { /// Formatter settings applied to all files in the workspace pub formatter: FormatterSettings, + /// Pglinter (database linter via pglinter extension) settings for the workspace + pub pglinter: PglinterSettings, + /// Type checking settings for the workspace pub typecheck: TypecheckSettings, @@ -277,6 +281,11 @@ impl Settings { )?; } + // pglinter part + if let Some(pglinter) = configuration.pglinter { + self.pglinter = to_pglinter_settings(PglinterConfiguration::from(pglinter)); + } + // typecheck part if let Some(typecheck) = configuration.typecheck { self.typecheck = to_typecheck_settings(TypecheckConfiguration::from(typecheck)); @@ -314,6 +323,11 @@ impl Settings { self.splinter.rules.as_ref().map(Cow::Borrowed) } + /// Returns pglinter rules. + pub fn as_pglinter_rules(&self) -> Option> { + self.pglinter.rules.as_ref().map(Cow::Borrowed) + } + /// It retrieves the severity based on the `code` of the rule and the current configuration. /// /// The code of the has the following pattern: `{group}/{rule_name}`. @@ -367,6 +381,13 @@ fn to_formatter_settings( }) } +fn to_pglinter_settings(conf: PglinterConfiguration) -> PglinterSettings { + PglinterSettings { + enabled: conf.enabled, + rules: Some(conf.rules), + } +} + fn to_typecheck_settings(conf: TypecheckConfiguration) -> TypecheckSettings { TypecheckSettings { search_path: conf.search_path.into_iter().collect(), @@ -567,6 +588,26 @@ impl Default for FormatterSettings { } } } + +/// Pglinter (database linter via pglinter extension) settings for the entire workspace +#[derive(Debug)] +pub struct PglinterSettings { + /// Disabled by default (pglinter extension might not be installed) + pub enabled: bool, + + /// List of rules + pub rules: Option, +} + +impl Default for PglinterSettings { + fn default() -> Self { + Self { + enabled: false, // Disabled by default since pglinter extension might not be installed + rules: Some(pgls_configuration::pglinter::Rules::default()), + } + } +} + /// Type checking settings for the entire workspace #[derive(Debug)] pub struct PlPgSqlCheckSettings { diff --git a/docs/schema.json b/docs/schema.json index a3408c773..2cd8c88a6 100644 --- a/docs/schema.json +++ b/docs/schema.json @@ -77,6 +77,17 @@ } ] }, + "pglinter": { + "description": "The configuration for pglinter", + "anyOf": [ + { + "$ref": "#/definitions/PglinterConfiguration" + }, + { + "type": "null" + } + ] + }, "plpgsqlCheck": { "description": "The configuration for type checking", "anyOf": [ @@ -124,6 +135,213 @@ }, "additionalProperties": false, "definitions": { + "Base": { + "description": "A list of rules that belong to this group", + "type": "object", + "properties": { + "all": { + "description": "It enables ALL rules for this group.", + "type": [ + "boolean", + "null" + ] + }, + "compositePrimaryKeyTooManyColumns": { + "description": "CompositePrimaryKeyTooManyColumns (B012): Detect tables with composite primary keys involving more than 4 columns", + "anyOf": [ + { + "$ref": "#/definitions/RuleConfiguration" + }, + { + "type": "null" + } + ] + }, + "howManyObjectsWithUppercase": { + "description": "HowManyObjectsWithUppercase (B005): Count number of objects with uppercase in name or in columns.", + "anyOf": [ + { + "$ref": "#/definitions/RuleConfiguration" + }, + { + "type": "null" + } + ] + }, + "howManyRedudantIndex": { + "description": "HowManyRedudantIndex (B002): Count number of redundant index vs nb index.", + "anyOf": [ + { + "$ref": "#/definitions/RuleConfiguration" + }, + { + "type": "null" + } + ] + }, + "howManyTableWithoutIndexOnFk": { + "description": "HowManyTableWithoutIndexOnFk (B003): Count number of tables without index on foreign key.", + "anyOf": [ + { + "$ref": "#/definitions/RuleConfiguration" + }, + { + "type": "null" + } + ] + }, + "howManyTableWithoutPrimaryKey": { + "description": "HowManyTableWithoutPrimaryKey (B001): Count number of tables without primary key.", + "anyOf": [ + { + "$ref": "#/definitions/RuleConfiguration" + }, + { + "type": "null" + } + ] + }, + "howManyTablesNeverSelected": { + "description": "HowManyTablesNeverSelected (B006): Count number of table(s) that has never been selected.", + "anyOf": [ + { + "$ref": "#/definitions/RuleConfiguration" + }, + { + "type": "null" + } + ] + }, + "howManyTablesWithFkMismatch": { + "description": "HowManyTablesWithFkMismatch (B008): Count number of tables with foreign keys that do not match the key reference type.", + "anyOf": [ + { + "$ref": "#/definitions/RuleConfiguration" + }, + { + "type": "null" + } + ] + }, + "howManyTablesWithFkOutsideSchema": { + "description": "HowManyTablesWithFkOutsideSchema (B007): Count number of tables with foreign keys outside their schema.", + "anyOf": [ + { + "$ref": "#/definitions/RuleConfiguration" + }, + { + "type": "null" + } + ] + }, + "howManyTablesWithReservedKeywords": { + "description": "HowManyTablesWithReservedKeywords (B010): Count number of database objects using reserved keywords in their names.", + "anyOf": [ + { + "$ref": "#/definitions/RuleConfiguration" + }, + { + "type": "null" + } + ] + }, + "howManyTablesWithSameTrigger": { + "description": "HowManyTablesWithSameTrigger (B009): Count number of tables using the same trigger vs nb table with their own triggers.", + "anyOf": [ + { + "$ref": "#/definitions/RuleConfiguration" + }, + { + "type": "null" + } + ] + }, + "howManyUnusedIndex": { + "description": "HowManyUnusedIndex (B004): Count number of unused index vs nb index (base on pg_stat_user_indexes, indexes associated to unique constraints are discard.)", + "anyOf": [ + { + "$ref": "#/definitions/RuleConfiguration" + }, + { + "type": "null" + } + ] + }, + "recommended": { + "description": "It enables the recommended rules for this group", + "type": [ + "boolean", + "null" + ] + }, + "severalTableOwnerInSchema": { + "description": "SeveralTableOwnerInSchema (B011): In a schema there are several tables owned by different owners.", + "anyOf": [ + { + "$ref": "#/definitions/RuleConfiguration" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + "Cluster": { + "description": "A list of rules that belong to this group", + "type": "object", + "properties": { + "all": { + "description": "It enables ALL rules for this group.", + "type": [ + "boolean", + "null" + ] + }, + "passwordEncryptionIsMd5": { + "description": "PasswordEncryptionIsMd5 (C003): This configuration is not secure anymore and will prevent an upgrade to Postgres 18. Warning, you will need to reset all passwords after this is changed to scram-sha-256.", + "anyOf": [ + { + "$ref": "#/definitions/RuleConfiguration" + }, + { + "type": "null" + } + ] + }, + "pgHbaEntriesWithMethodTrustOrPasswordShouldNotExists": { + "description": "PgHbaEntriesWithMethodTrustOrPasswordShouldNotExists (C002): This configuration is extremely insecure and should only be used in a controlled, non-production environment for testing purposes. In a production environment, you should use more secure authentication methods such as md5, scram-sha-256, or cert, and restrict access to trusted IP addresses only.", + "anyOf": [ + { + "$ref": "#/definitions/RuleConfiguration" + }, + { + "type": "null" + } + ] + }, + "pgHbaEntriesWithMethodTrustShouldNotExists": { + "description": "PgHbaEntriesWithMethodTrustShouldNotExists (C001): This configuration is extremely insecure and should only be used in a controlled, non-production environment for testing purposes. In a production environment, you should use more secure authentication methods such as md5, scram-sha-256, or cert, and restrict access to trusted IP addresses only.", + "anyOf": [ + { + "$ref": "#/definitions/RuleConfiguration" + }, + { + "type": "null" + } + ] + }, + "recommended": { + "description": "It enables the recommended rules for this group", + "type": [ + "boolean", + "null" + ] + } + }, + "additionalProperties": false + }, "DatabaseConfiguration": { "description": "The configuration of the database connection.", "type": "object", @@ -565,6 +783,80 @@ }, "additionalProperties": false }, + "PglinterConfiguration": { + "type": "object", + "properties": { + "enabled": { + "description": "if `false`, it disables the feature and the linter won't be executed. `true` by default", + "type": [ + "boolean", + "null" + ] + }, + "rules": { + "description": "List of rules", + "anyOf": [ + { + "$ref": "#/definitions/PglinterRules" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + "PglinterRules": { + "type": "object", + "properties": { + "all": { + "description": "It enables ALL rules. The rules that belong to `nursery` won't be enabled.", + "type": [ + "boolean", + "null" + ] + }, + "base": { + "anyOf": [ + { + "$ref": "#/definitions/Base" + }, + { + "type": "null" + } + ] + }, + "cluster": { + "anyOf": [ + { + "$ref": "#/definitions/Cluster" + }, + { + "type": "null" + } + ] + }, + "recommended": { + "description": "It enables the lint rules recommended by Postgres Language Server. `true` by default.", + "type": [ + "boolean", + "null" + ] + }, + "schema": { + "anyOf": [ + { + "$ref": "#/definitions/Schema" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, "PlPgSqlCheckConfiguration": { "description": "The configuration for type checking.", "type": "object", @@ -1025,6 +1317,82 @@ }, "additionalProperties": false }, + "Schema": { + "description": "A list of rules that belong to this group", + "type": "object", + "properties": { + "all": { + "description": "It enables ALL rules for this group.", + "type": [ + "boolean", + "null" + ] + }, + "ownerSchemaIsInternalRole": { + "description": "OwnerSchemaIsInternalRole (S004): Owner of schema should not be any internal pg roles, or owner is a superuser (not sure it is necesary).", + "anyOf": [ + { + "$ref": "#/definitions/RuleConfiguration" + }, + { + "type": "null" + } + ] + }, + "recommended": { + "description": "It enables the recommended rules for this group", + "type": [ + "boolean", + "null" + ] + }, + "schemaOwnerDoNotMatchTableOwner": { + "description": "SchemaOwnerDoNotMatchTableOwner (S005): The schema owner and tables in the schema do not match.", + "anyOf": [ + { + "$ref": "#/definitions/RuleConfiguration" + }, + { + "type": "null" + } + ] + }, + "schemaPrefixedOrSuffixedWithEnvt": { + "description": "SchemaPrefixedOrSuffixedWithEnvt (S002): The schema is prefixed with one of staging,stg,preprod,prod,sandbox,sbox string. Means that when you refresh your preprod, staging environments from production, you have to rename the target schema from prod_ to stg_ or something like. It is possible, but it is never easy.", + "anyOf": [ + { + "$ref": "#/definitions/RuleConfiguration" + }, + { + "type": "null" + } + ] + }, + "schemaWithDefaultRoleNotGranted": { + "description": "SchemaWithDefaultRoleNotGranted (S001): The schema has no default role. Means that futur table will not be granted through a role. So you will have to re-execute grants on it.", + "anyOf": [ + { + "$ref": "#/definitions/RuleConfiguration" + }, + { + "type": "null" + } + ] + }, + "unsecuredPublicSchema": { + "description": "UnsecuredPublicSchema (S003): Only authorized users should be allowed to create objects.", + "anyOf": [ + { + "$ref": "#/definitions/RuleConfiguration" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, "Security": { "description": "A list of rules that belong to this group", "type": "object", diff --git a/justfile b/justfile index 3cac92a2f..fef1c8ec7 100644 --- a/justfile +++ b/justfile @@ -29,6 +29,7 @@ gen-lint: cargo run -p xtask_codegen -- configuration cargo run -p xtask_codegen -- bindings cargo run -p xtask_codegen -- splinter + cargo run -p xtask_codegen -- pglinter cargo run -p rules_check cargo run -p docs_codegen just format diff --git a/packages/@postgres-language-server/backend-jsonrpc/src/workspace.ts b/packages/@postgres-language-server/backend-jsonrpc/src/workspace.ts index 912162da0..3a2aeafeb 100644 --- a/packages/@postgres-language-server/backend-jsonrpc/src/workspace.ts +++ b/packages/@postgres-language-server/backend-jsonrpc/src/workspace.ts @@ -95,6 +95,28 @@ export type Category = | "lint/safety/requireConcurrentIndexDeletion" | "lint/safety/runningStatementWhileHoldingAccessExclusive" | "lint/safety/transactionNesting" + | "pglinter/extensionNotInstalled" + | "pglinter/ruleDisabledInExtension" + | "pglinter/base/compositePrimaryKeyTooManyColumns" + | "pglinter/base/howManyObjectsWithUppercase" + | "pglinter/base/howManyRedudantIndex" + | "pglinter/base/howManyTableWithoutIndexOnFk" + | "pglinter/base/howManyTableWithoutPrimaryKey" + | "pglinter/base/howManyTablesNeverSelected" + | "pglinter/base/howManyTablesWithFkMismatch" + | "pglinter/base/howManyTablesWithFkOutsideSchema" + | "pglinter/base/howManyTablesWithReservedKeywords" + | "pglinter/base/howManyTablesWithSameTrigger" + | "pglinter/base/howManyUnusedIndex" + | "pglinter/base/severalTableOwnerInSchema" + | "pglinter/cluster/passwordEncryptionIsMd5" + | "pglinter/cluster/pgHbaEntriesWithMethodTrustOrPasswordShouldNotExists" + | "pglinter/cluster/pgHbaEntriesWithMethodTrustShouldNotExists" + | "pglinter/schema/ownerSchemaIsInternalRole" + | "pglinter/schema/schemaOwnerDoNotMatchTableOwner" + | "pglinter/schema/schemaPrefixedOrSuffixedWithEnvt" + | "pglinter/schema/schemaWithDefaultRoleNotGranted" + | "pglinter/schema/unsecuredPublicSchema" | "splinter/performance/authRlsInitplan" | "splinter/performance/duplicateIndex" | "splinter/performance/multiplePermissivePolicies" @@ -136,7 +158,11 @@ export type Category = | "lint/safety" | "splinter" | "splinter/performance" - | "splinter/security"; + | "splinter/security" + | "pglinter" + | "pglinter/base" + | "pglinter/cluster" + | "pglinter/schema"; export interface Location { path?: Resource_for_String; sourceCode?: string; @@ -317,6 +343,10 @@ export interface PartialConfiguration { * Configure migrations */ migrations?: PartialMigrationsConfiguration; + /** + * The configuration for pglinter + */ + pglinter?: PartialPglinterConfiguration; /** * The configuration for type checking */ @@ -458,6 +488,16 @@ export interface PartialMigrationsConfiguration { */ migrationsDir?: string; } +export interface PartialPglinterConfiguration { + /** + * if `false`, it disables the feature and the linter won't be executed. `true` by default + */ + enabled?: boolean; + /** + * List of rules + */ + rules?: PglinterRules; +} /** * The configuration for type checking. */ @@ -540,6 +580,19 @@ export interface LinterRules { recommended?: boolean; safety?: Safety; } +export interface PglinterRules { + /** + * It enables ALL rules. The rules that belong to `nursery` won't be enabled. + */ + all?: boolean; + base?: Base; + cluster?: Cluster; + /** + * It enables the lint rules recommended by Postgres Language Server. `true` by default. + */ + recommended?: boolean; + schema?: Schema; +} export interface SplinterRules { /** * It enables ALL rules. The rules that belong to `nursery` won't be enabled. @@ -698,6 +751,125 @@ export interface Safety { */ transactionNesting?: RuleConfiguration_for_Null; } +/** + * A list of rules that belong to this group + */ +export interface Base { + /** + * It enables ALL rules for this group. + */ + all?: boolean; + /** + * CompositePrimaryKeyTooManyColumns (B012): Detect tables with composite primary keys involving more than 4 columns + */ + compositePrimaryKeyTooManyColumns?: RuleConfiguration_for_Null; + /** + * HowManyObjectsWithUppercase (B005): Count number of objects with uppercase in name or in columns. + */ + howManyObjectsWithUppercase?: RuleConfiguration_for_Null; + /** + * HowManyRedudantIndex (B002): Count number of redundant index vs nb index. + */ + howManyRedudantIndex?: RuleConfiguration_for_Null; + /** + * HowManyTableWithoutIndexOnFk (B003): Count number of tables without index on foreign key. + */ + howManyTableWithoutIndexOnFk?: RuleConfiguration_for_Null; + /** + * HowManyTableWithoutPrimaryKey (B001): Count number of tables without primary key. + */ + howManyTableWithoutPrimaryKey?: RuleConfiguration_for_Null; + /** + * HowManyTablesNeverSelected (B006): Count number of table(s) that has never been selected. + */ + howManyTablesNeverSelected?: RuleConfiguration_for_Null; + /** + * HowManyTablesWithFkMismatch (B008): Count number of tables with foreign keys that do not match the key reference type. + */ + howManyTablesWithFkMismatch?: RuleConfiguration_for_Null; + /** + * HowManyTablesWithFkOutsideSchema (B007): Count number of tables with foreign keys outside their schema. + */ + howManyTablesWithFkOutsideSchema?: RuleConfiguration_for_Null; + /** + * HowManyTablesWithReservedKeywords (B010): Count number of database objects using reserved keywords in their names. + */ + howManyTablesWithReservedKeywords?: RuleConfiguration_for_Null; + /** + * HowManyTablesWithSameTrigger (B009): Count number of tables using the same trigger vs nb table with their own triggers. + */ + howManyTablesWithSameTrigger?: RuleConfiguration_for_Null; + /** + * HowManyUnusedIndex (B004): Count number of unused index vs nb index (base on pg_stat_user_indexes, indexes associated to unique constraints are discard.) + */ + howManyUnusedIndex?: RuleConfiguration_for_Null; + /** + * It enables the recommended rules for this group + */ + recommended?: boolean; + /** + * SeveralTableOwnerInSchema (B011): In a schema there are several tables owned by different owners. + */ + severalTableOwnerInSchema?: RuleConfiguration_for_Null; +} +/** + * A list of rules that belong to this group + */ +export interface Cluster { + /** + * It enables ALL rules for this group. + */ + all?: boolean; + /** + * PasswordEncryptionIsMd5 (C003): This configuration is not secure anymore and will prevent an upgrade to Postgres 18. Warning, you will need to reset all passwords after this is changed to scram-sha-256. + */ + passwordEncryptionIsMd5?: RuleConfiguration_for_Null; + /** + * PgHbaEntriesWithMethodTrustOrPasswordShouldNotExists (C002): This configuration is extremely insecure and should only be used in a controlled, non-production environment for testing purposes. In a production environment, you should use more secure authentication methods such as md5, scram-sha-256, or cert, and restrict access to trusted IP addresses only. + */ + pgHbaEntriesWithMethodTrustOrPasswordShouldNotExists?: RuleConfiguration_for_Null; + /** + * PgHbaEntriesWithMethodTrustShouldNotExists (C001): This configuration is extremely insecure and should only be used in a controlled, non-production environment for testing purposes. In a production environment, you should use more secure authentication methods such as md5, scram-sha-256, or cert, and restrict access to trusted IP addresses only. + */ + pgHbaEntriesWithMethodTrustShouldNotExists?: RuleConfiguration_for_Null; + /** + * It enables the recommended rules for this group + */ + recommended?: boolean; +} +/** + * A list of rules that belong to this group + */ +export interface Schema { + /** + * It enables ALL rules for this group. + */ + all?: boolean; + /** + * OwnerSchemaIsInternalRole (S004): Owner of schema should not be any internal pg roles, or owner is a superuser (not sure it is necesary). + */ + ownerSchemaIsInternalRole?: RuleConfiguration_for_Null; + /** + * It enables the recommended rules for this group + */ + recommended?: boolean; + /** + * SchemaOwnerDoNotMatchTableOwner (S005): The schema owner and tables in the schema do not match. + */ + schemaOwnerDoNotMatchTableOwner?: RuleConfiguration_for_Null; + /** + * SchemaPrefixedOrSuffixedWithEnvt (S002): The schema is prefixed with one of staging,stg,preprod,prod,sandbox,sbox string. Means that when you refresh your preprod, staging environments from production, you have to rename the target schema from prod_ to stg_ or something like. It is possible, but it is never easy. + */ + schemaPrefixedOrSuffixedWithEnvt?: RuleConfiguration_for_Null; + /** + * SchemaWithDefaultRoleNotGranted (S001): The schema has no default role. Means that futur table will not be granted through a role. So you will have to re-execute grants on it. + */ + schemaWithDefaultRoleNotGranted?: RuleConfiguration_for_Null; + /** + * UnsecuredPublicSchema (S003): Only authorized users should be allowed to create objects. + */ + unsecuredPublicSchema?: RuleConfiguration_for_Null; +} /** * A list of rules that belong to this group */ diff --git a/packages/@postgrestools/backend-jsonrpc/src/workspace.ts b/packages/@postgrestools/backend-jsonrpc/src/workspace.ts index 912162da0..3a2aeafeb 100644 --- a/packages/@postgrestools/backend-jsonrpc/src/workspace.ts +++ b/packages/@postgrestools/backend-jsonrpc/src/workspace.ts @@ -95,6 +95,28 @@ export type Category = | "lint/safety/requireConcurrentIndexDeletion" | "lint/safety/runningStatementWhileHoldingAccessExclusive" | "lint/safety/transactionNesting" + | "pglinter/extensionNotInstalled" + | "pglinter/ruleDisabledInExtension" + | "pglinter/base/compositePrimaryKeyTooManyColumns" + | "pglinter/base/howManyObjectsWithUppercase" + | "pglinter/base/howManyRedudantIndex" + | "pglinter/base/howManyTableWithoutIndexOnFk" + | "pglinter/base/howManyTableWithoutPrimaryKey" + | "pglinter/base/howManyTablesNeverSelected" + | "pglinter/base/howManyTablesWithFkMismatch" + | "pglinter/base/howManyTablesWithFkOutsideSchema" + | "pglinter/base/howManyTablesWithReservedKeywords" + | "pglinter/base/howManyTablesWithSameTrigger" + | "pglinter/base/howManyUnusedIndex" + | "pglinter/base/severalTableOwnerInSchema" + | "pglinter/cluster/passwordEncryptionIsMd5" + | "pglinter/cluster/pgHbaEntriesWithMethodTrustOrPasswordShouldNotExists" + | "pglinter/cluster/pgHbaEntriesWithMethodTrustShouldNotExists" + | "pglinter/schema/ownerSchemaIsInternalRole" + | "pglinter/schema/schemaOwnerDoNotMatchTableOwner" + | "pglinter/schema/schemaPrefixedOrSuffixedWithEnvt" + | "pglinter/schema/schemaWithDefaultRoleNotGranted" + | "pglinter/schema/unsecuredPublicSchema" | "splinter/performance/authRlsInitplan" | "splinter/performance/duplicateIndex" | "splinter/performance/multiplePermissivePolicies" @@ -136,7 +158,11 @@ export type Category = | "lint/safety" | "splinter" | "splinter/performance" - | "splinter/security"; + | "splinter/security" + | "pglinter" + | "pglinter/base" + | "pglinter/cluster" + | "pglinter/schema"; export interface Location { path?: Resource_for_String; sourceCode?: string; @@ -317,6 +343,10 @@ export interface PartialConfiguration { * Configure migrations */ migrations?: PartialMigrationsConfiguration; + /** + * The configuration for pglinter + */ + pglinter?: PartialPglinterConfiguration; /** * The configuration for type checking */ @@ -458,6 +488,16 @@ export interface PartialMigrationsConfiguration { */ migrationsDir?: string; } +export interface PartialPglinterConfiguration { + /** + * if `false`, it disables the feature and the linter won't be executed. `true` by default + */ + enabled?: boolean; + /** + * List of rules + */ + rules?: PglinterRules; +} /** * The configuration for type checking. */ @@ -540,6 +580,19 @@ export interface LinterRules { recommended?: boolean; safety?: Safety; } +export interface PglinterRules { + /** + * It enables ALL rules. The rules that belong to `nursery` won't be enabled. + */ + all?: boolean; + base?: Base; + cluster?: Cluster; + /** + * It enables the lint rules recommended by Postgres Language Server. `true` by default. + */ + recommended?: boolean; + schema?: Schema; +} export interface SplinterRules { /** * It enables ALL rules. The rules that belong to `nursery` won't be enabled. @@ -698,6 +751,125 @@ export interface Safety { */ transactionNesting?: RuleConfiguration_for_Null; } +/** + * A list of rules that belong to this group + */ +export interface Base { + /** + * It enables ALL rules for this group. + */ + all?: boolean; + /** + * CompositePrimaryKeyTooManyColumns (B012): Detect tables with composite primary keys involving more than 4 columns + */ + compositePrimaryKeyTooManyColumns?: RuleConfiguration_for_Null; + /** + * HowManyObjectsWithUppercase (B005): Count number of objects with uppercase in name or in columns. + */ + howManyObjectsWithUppercase?: RuleConfiguration_for_Null; + /** + * HowManyRedudantIndex (B002): Count number of redundant index vs nb index. + */ + howManyRedudantIndex?: RuleConfiguration_for_Null; + /** + * HowManyTableWithoutIndexOnFk (B003): Count number of tables without index on foreign key. + */ + howManyTableWithoutIndexOnFk?: RuleConfiguration_for_Null; + /** + * HowManyTableWithoutPrimaryKey (B001): Count number of tables without primary key. + */ + howManyTableWithoutPrimaryKey?: RuleConfiguration_for_Null; + /** + * HowManyTablesNeverSelected (B006): Count number of table(s) that has never been selected. + */ + howManyTablesNeverSelected?: RuleConfiguration_for_Null; + /** + * HowManyTablesWithFkMismatch (B008): Count number of tables with foreign keys that do not match the key reference type. + */ + howManyTablesWithFkMismatch?: RuleConfiguration_for_Null; + /** + * HowManyTablesWithFkOutsideSchema (B007): Count number of tables with foreign keys outside their schema. + */ + howManyTablesWithFkOutsideSchema?: RuleConfiguration_for_Null; + /** + * HowManyTablesWithReservedKeywords (B010): Count number of database objects using reserved keywords in their names. + */ + howManyTablesWithReservedKeywords?: RuleConfiguration_for_Null; + /** + * HowManyTablesWithSameTrigger (B009): Count number of tables using the same trigger vs nb table with their own triggers. + */ + howManyTablesWithSameTrigger?: RuleConfiguration_for_Null; + /** + * HowManyUnusedIndex (B004): Count number of unused index vs nb index (base on pg_stat_user_indexes, indexes associated to unique constraints are discard.) + */ + howManyUnusedIndex?: RuleConfiguration_for_Null; + /** + * It enables the recommended rules for this group + */ + recommended?: boolean; + /** + * SeveralTableOwnerInSchema (B011): In a schema there are several tables owned by different owners. + */ + severalTableOwnerInSchema?: RuleConfiguration_for_Null; +} +/** + * A list of rules that belong to this group + */ +export interface Cluster { + /** + * It enables ALL rules for this group. + */ + all?: boolean; + /** + * PasswordEncryptionIsMd5 (C003): This configuration is not secure anymore and will prevent an upgrade to Postgres 18. Warning, you will need to reset all passwords after this is changed to scram-sha-256. + */ + passwordEncryptionIsMd5?: RuleConfiguration_for_Null; + /** + * PgHbaEntriesWithMethodTrustOrPasswordShouldNotExists (C002): This configuration is extremely insecure and should only be used in a controlled, non-production environment for testing purposes. In a production environment, you should use more secure authentication methods such as md5, scram-sha-256, or cert, and restrict access to trusted IP addresses only. + */ + pgHbaEntriesWithMethodTrustOrPasswordShouldNotExists?: RuleConfiguration_for_Null; + /** + * PgHbaEntriesWithMethodTrustShouldNotExists (C001): This configuration is extremely insecure and should only be used in a controlled, non-production environment for testing purposes. In a production environment, you should use more secure authentication methods such as md5, scram-sha-256, or cert, and restrict access to trusted IP addresses only. + */ + pgHbaEntriesWithMethodTrustShouldNotExists?: RuleConfiguration_for_Null; + /** + * It enables the recommended rules for this group + */ + recommended?: boolean; +} +/** + * A list of rules that belong to this group + */ +export interface Schema { + /** + * It enables ALL rules for this group. + */ + all?: boolean; + /** + * OwnerSchemaIsInternalRole (S004): Owner of schema should not be any internal pg roles, or owner is a superuser (not sure it is necesary). + */ + ownerSchemaIsInternalRole?: RuleConfiguration_for_Null; + /** + * It enables the recommended rules for this group + */ + recommended?: boolean; + /** + * SchemaOwnerDoNotMatchTableOwner (S005): The schema owner and tables in the schema do not match. + */ + schemaOwnerDoNotMatchTableOwner?: RuleConfiguration_for_Null; + /** + * SchemaPrefixedOrSuffixedWithEnvt (S002): The schema is prefixed with one of staging,stg,preprod,prod,sandbox,sbox string. Means that when you refresh your preprod, staging environments from production, you have to rename the target schema from prod_ to stg_ or something like. It is possible, but it is never easy. + */ + schemaPrefixedOrSuffixedWithEnvt?: RuleConfiguration_for_Null; + /** + * SchemaWithDefaultRoleNotGranted (S001): The schema has no default role. Means that futur table will not be granted through a role. So you will have to re-execute grants on it. + */ + schemaWithDefaultRoleNotGranted?: RuleConfiguration_for_Null; + /** + * UnsecuredPublicSchema (S003): Only authorized users should be allowed to create objects. + */ + unsecuredPublicSchema?: RuleConfiguration_for_Null; +} /** * A list of rules that belong to this group */ diff --git a/xtask/codegen/Cargo.toml b/xtask/codegen/Cargo.toml index c5e95ebe4..24898d319 100644 --- a/xtask/codegen/Cargo.toml +++ b/xtask/codegen/Cargo.toml @@ -16,9 +16,11 @@ pgls_analyse = { workspace = true } pgls_analyser = { workspace = true } pgls_diagnostics = { workspace = true } pgls_env = { workspace = true } +pgls_pglinter = { workspace = true } pgls_splinter = { workspace = true } pgls_workspace = { workspace = true, features = ["schema"] } proc-macro2 = { workspace = true, features = ["span-locations"] } pulldown-cmark = { version = "0.12.2" } quote = "1.0.36" +regex = "1.11" xtask = { path = '../', version = "0.0" } diff --git a/xtask/codegen/src/generate_configuration.rs b/xtask/codegen/src/generate_configuration.rs index e93380780..ad5b21159 100644 --- a/xtask/codegen/src/generate_configuration.rs +++ b/xtask/codegen/src/generate_configuration.rs @@ -81,7 +81,7 @@ const TOOLS: &[ToolConfig] = &[ ToolConfig::new("linter", RuleCategory::Lint, true), ToolConfig::new("assists", RuleCategory::Action, true), ToolConfig::new("splinter", RuleCategory::Lint, false), // Database linter, doesn't handle files - ToolConfig::new("pglinter", RuleCategory::Lint, true), + ToolConfig::new("pglinter", RuleCategory::Lint, false), // Database linter via pglinter extension ]; /// Visitor that collects rules for a specific category @@ -121,6 +121,7 @@ impl RegistryVisitor for CategoryRulesVisitor { pub fn generate_rules_configuration(mode: Mode) -> Result<()> { generate_tool_configuration(mode, "linter")?; generate_tool_configuration(mode, "splinter")?; + generate_tool_configuration(mode, "pglinter")?; Ok(()) } @@ -140,8 +141,8 @@ pub fn generate_tool_configuration(mode: Mode, tool_name: &str) -> Result<()> { match tool.name { "linter" => pgls_analyser::visit_registry(&mut visitor), "splinter" => pgls_splinter::registry::visit_registry(&mut visitor), + "pglinter" => pgls_pglinter::registry::visit_registry(&mut visitor), "assists" => unimplemented!("Assists rules not yet implemented"), - "pglinter" => unimplemented!("PGLinter rules not yet implemented"), _ => unreachable!(), } @@ -643,9 +644,12 @@ fn generate_lint_group_struct( } // For splinter rules, use SplinterRuleOptions for the shared ignore patterns + // For pglinter rules, use () as options since they don't have configurable options // For linter rules, use pgls_analyser::options::#rule_name let rule_option_type = if tool_name == "splinter" { quote! { crate::splinter::SplinterRuleOptions } + } else if tool_name == "pglinter" { + quote! { () } } else { quote! { pgls_analyser::options::#rule_name } }; diff --git a/xtask/codegen/src/generate_pglinter.rs b/xtask/codegen/src/generate_pglinter.rs new file mode 100644 index 000000000..bbf4289ab --- /dev/null +++ b/xtask/codegen/src/generate_pglinter.rs @@ -0,0 +1,687 @@ +use anyhow::{Context, Result}; +use biome_string_case::Case; +use quote::{format_ident, quote}; +use regex::Regex; +use std::collections::BTreeMap; +use std::path::Path; +use xtask::{glue::fs2, project_root, Mode}; + +use crate::update; + +/// Metadata extracted from rules.sql INSERT statements +#[derive(Debug, Clone)] +struct PglinterRuleMeta { + /// Rule name in PascalCase (e.g., "HowManyTableWithoutPrimaryKey") + name: String, + /// Rule name in snake_case (e.g., "how_many_table_without_primary_key") + snake_name: String, + /// Rule name in camelCase (e.g., "howManyTableWithoutPrimaryKey") + camel_name: String, + /// Rule code (e.g., "B001") + code: String, + /// Scope: BASE, SCHEMA, or CLUSTER + scope: String, + /// Description of the rule + description: String, + /// Message template with placeholders + message: String, + /// Suggested fixes + fixes: Vec, + /// Warning threshold percentage + warning_level: i32, + /// Error threshold percentage + error_level: i32, +} + +/// Parse pglinter rules from rules.sql and generate Rust code +pub fn generate_pglinter() -> Result<()> { + let rules_sql_path = project_root().join("pglinter_repo/sql/rules.sql"); + + if !rules_sql_path.exists() { + anyhow::bail!( + "pglinter_repo/sql/rules.sql not found. Clone pglinter repo first: git clone https://github.com/pmpetit/pglinter pglinter_repo" + ); + } + + let sql_content = fs2::read_to_string(&rules_sql_path)?; + let rules = parse_rules_sql(&sql_content)?; + + // Generate rule files + generate_rule_trait()?; + generate_rule_files(&rules)?; + generate_registry(&rules)?; + update_categories_file(&rules)?; + + Ok(()) +} + +/// Parse INSERT statements from rules.sql to extract rule metadata +fn parse_rules_sql(content: &str) -> Result> { + let mut rules = BTreeMap::new(); + + // Normalize the content: remove newlines within parentheses to make regex easier + // This handles multi-line ARRAY declarations + let normalized = normalize_sql_values(content); + + // Use regex to find value tuples + // Pattern: ('Name', 'CODE', num, num, 'SCOPE', 'desc', 'msg', ARRAY[...]) + let value_pattern = Regex::new( + r#"\(\s*'([^']+)',\s*'([^']+)',\s*(\d+),\s*(\d+),\s*'([^']+)',\s*'([^']+)',\s*'([^']+)',\s*ARRAY\s*\[(.*?)\]\s*\)"#, + )?; + + for caps in value_pattern.captures_iter(&normalized) { + let name = caps.get(1).unwrap().as_str().to_string(); + let code = caps.get(2).unwrap().as_str().to_string(); + let warning_level: i32 = caps.get(3).unwrap().as_str().parse()?; + let error_level: i32 = caps.get(4).unwrap().as_str().parse()?; + let scope = caps.get(5).unwrap().as_str().to_string(); + let description = caps + .get(6) + .unwrap() + .as_str() + .replace("''", "'") // Unescape single quotes + .to_string(); + let message = caps.get(7).unwrap().as_str().to_string(); + let fixes_str = caps.get(8).unwrap().as_str(); + + // Parse fixes array + let fixes: Vec = parse_fixes_array(fixes_str); + + let snake_name = Case::Snake.convert(&name); + let camel_name = to_camel_case(&name); + + let meta = PglinterRuleMeta { + name, + snake_name: snake_name.clone(), + camel_name, + code, + scope, + description, + message, + fixes, + warning_level, + error_level, + }; + + rules.insert(snake_name, meta); + } + + if rules.is_empty() { + anyhow::bail!("No rules found in rules.sql. Check the file format."); + } + + Ok(rules) +} + +/// Normalize SQL content by joining lines within value tuples +fn normalize_sql_values(content: &str) -> String { + let mut result = String::new(); + let mut in_value = false; + let mut paren_depth = 0; + + for c in content.chars() { + match c { + '(' => { + paren_depth += 1; + in_value = true; + result.push(c); + } + ')' => { + paren_depth -= 1; + if paren_depth == 0 { + in_value = false; + } + result.push(c); + } + '\n' | '\r' if in_value => { + result.push(' '); // Replace newlines with spaces inside values + } + _ => result.push(c), + } + } + + result +} + +/// Parse ARRAY['fix1', 'fix2'] into Vec +fn parse_fixes_array(s: &str) -> Vec { + let fix_pattern = Regex::new(r#"'([^']+)'"#).unwrap(); + fix_pattern + .captures_iter(s) + .map(|cap| cap.get(1).unwrap().as_str().to_string()) + .collect() +} + +/// Convert PascalCase to camelCase +fn to_camel_case(s: &str) -> String { + let mut chars = s.chars(); + match chars.next() { + None => String::new(), + Some(first) => first.to_lowercase().collect::() + chars.as_str(), + } +} + +/// Map scope to category directory name +fn scope_to_category(scope: &str) -> &'static str { + match scope { + "BASE" => "base", + "SCHEMA" => "schema", + "CLUSTER" => "cluster", + _ => "base", + } +} + +/// Generate src/rule.rs with PglinterRule trait +fn generate_rule_trait() -> Result<()> { + let rule_path = project_root().join("crates/pgls_pglinter/src/rule.rs"); + + let content = quote! { + //! Generated file, do not edit by hand, see `xtask/codegen` + + use pgls_analyse::RuleMeta; + + /// Trait for pglinter (database-level) rules + /// + /// Pglinter rules are different from linter rules: + /// - They execute SQL queries against the database via pglinter extension + /// - They don't have AST-based execution + /// - Rule logic is in the pglinter Postgres extension + /// - Threshold configuration (warning/error levels) is handled by pglinter extension + pub trait PglinterRule: RuleMeta { + /// Rule code (e.g., "B001", "S001", "C001") + const CODE: &'static str; + + /// Rule scope (BASE, SCHEMA, or CLUSTER) + const SCOPE: &'static str; + + /// Description of what the rule detects + const DESCRIPTION: &'static str; + + /// Suggested fixes for violations + const FIXES: &'static [&'static str]; + } + }; + + let formatted = xtask::reformat(content)?; + update(&rule_path, &formatted, &Mode::Overwrite)?; + + Ok(()) +} + +/// Generate rule files in src/rules/{category}/{rule_name}.rs +fn generate_rule_files(rules: &BTreeMap) -> Result<()> { + let rules_dir = project_root().join("crates/pgls_pglinter/src/rules"); + + // Group rules by scope/category + let mut rules_by_category: BTreeMap> = BTreeMap::new(); + for rule in rules.values() { + let category = scope_to_category(&rule.scope).to_string(); + rules_by_category.entry(category).or_default().push(rule); + } + + // Generate category directories and files + for (category, category_rules) in &rules_by_category { + let category_dir = rules_dir.join(category); + fs2::create_dir_all(&category_dir)?; + + // Generate individual rule files + for rule in category_rules { + generate_rule_file(&category_dir, rule)?; + } + + // Generate category mod.rs + generate_category_mod(&category_dir, category, category_rules)?; + } + + // Generate main rules/mod.rs + generate_rules_mod(&rules_dir, &rules_by_category)?; + + Ok(()) +} + +/// Generate individual rule file +fn generate_rule_file(category_dir: &Path, rule: &PglinterRuleMeta) -> Result<()> { + let rule_file = category_dir.join(format!("{}.rs", rule.snake_name)); + + let struct_name = format_ident!("{}", rule.name); + let camel_name = &rule.camel_name; + let code = &rule.code; + let scope = &rule.scope; + let description = &rule.description; + let warning_level = rule.warning_level; + let error_level = rule.error_level; + let category = scope_to_category(&rule.scope); + + // Create fixes as static slice + let fixes: Vec<&str> = rule.fixes.iter().map(|s| s.as_str()).collect(); + + // Build doc string + let doc_string = format!( + r#"# {} ({}) + +{} + +## Configuration + +Enable or disable this rule in your configuration: + +```json +{{ + "pglinter": {{ + "rules": {{ + "{}": {{ + "{}": "warn" + }} + }} + }} +}} +``` + +## Thresholds + +- Warning level: {}% +- Error level: {}% + +## Fixes + +{} + +## Documentation + +See: "#, + rule.name, + code, + description, + category, + camel_name, + warning_level, + error_level, + rule.fixes + .iter() + .map(|f| format!("- {f}")) + .collect::>() + .join("\n"), + code.to_lowercase(), + ); + + let content = quote! { + //! Generated file, do not edit by hand, see `xtask/codegen` + + use crate::rule::PglinterRule; + + ::pgls_analyse::declare_rule! { + #[doc = #doc_string] + pub #struct_name { + version: "1.0.0", + name: #camel_name, + severity: pgls_diagnostics::Severity::Warning, + recommended: true, + } + } + + impl PglinterRule for #struct_name { + const CODE: &'static str = #code; + const SCOPE: &'static str = #scope; + const DESCRIPTION: &'static str = #description; + const FIXES: &'static [&'static str] = &[#(#fixes),*]; + } + }; + + let formatted = xtask::reformat(content)?; + update(&rule_file, &formatted, &Mode::Overwrite)?; + + Ok(()) +} + +/// Generate category mod.rs that exports all rules +fn generate_category_mod( + category_dir: &Path, + category: &str, + rules: &[&PglinterRuleMeta], +) -> Result<()> { + let mod_file = category_dir.join("mod.rs"); + + let category_title = Case::Pascal.convert(category); + let category_struct = format_ident!("{}", category_title); + + // Generate mod declarations + let mod_names: Vec<_> = rules + .iter() + .map(|r| format_ident!("{}", r.snake_name)) + .collect(); + + // Generate rule paths for declare_lint_group! + let rule_paths: Vec<_> = rules + .iter() + .map(|r| { + let mod_name = format_ident!("{}", r.snake_name); + let struct_name = format_ident!("{}", r.name); + quote! { self::#mod_name::#struct_name } + }) + .collect(); + + let content = quote! { + //! Generated file, do not edit by hand, see `xtask/codegen` + + #( pub mod #mod_names; )* + + ::pgls_analyse::declare_lint_group! { + pub #category_struct { + name: #category, + rules: [ + #( #rule_paths, )* + ] + } + } + }; + + let formatted = xtask::reformat(content)?; + update(&mod_file, &formatted, &Mode::Overwrite)?; + + Ok(()) +} + +/// Generate main rules/mod.rs +fn generate_rules_mod( + rules_dir: &Path, + rules_by_category: &BTreeMap>, +) -> Result<()> { + let mod_file = rules_dir.join("mod.rs"); + + let category_mods: Vec<_> = rules_by_category + .keys() + .map(|cat| { + let mod_name = format_ident!("{}", cat); + quote! { pub mod #mod_name; } + }) + .collect(); + + // Generate group paths for declare_category! + let group_paths: Vec<_> = rules_by_category + .keys() + .map(|cat| { + let mod_name = format_ident!("{}", cat); + let group_name = format_ident!("{}", Case::Pascal.convert(cat)); + quote! { self::#mod_name::#group_name } + }) + .collect(); + + let content = quote! { + //! Generated file, do not edit by hand, see `xtask/codegen` + + #( #category_mods )* + + ::pgls_analyse::declare_category! { + pub PgLinter { + kind: Lint, + groups: [ + #( #group_paths, )* + ] + } + } + }; + + let formatted = xtask::reformat(content)?; + update(&mod_file, &formatted, &Mode::Overwrite)?; + + Ok(()) +} + +/// Generate src/registry.rs with visit_registry() and get_rule_category() +fn generate_registry(rules: &BTreeMap) -> Result<()> { + let registry_path = project_root().join("crates/pgls_pglinter/src/registry.rs"); + + // Generate match arms for rule code lookup (camelCase → code) + let code_arms: Vec<_> = rules + .values() + .map(|rule| { + let camel_name = &rule.camel_name; + let code = &rule.code; + quote! { + #camel_name => Some(#code) + } + }) + .collect(); + + // Generate match arms for category lookup (code → &'static Category) + let category_arms: Vec<_> = rules + .values() + .map(|rule| { + let code = &rule.code; + let category = scope_to_category(&rule.scope); + let camel_name = &rule.camel_name; + let category_path = format!("pglinter/{category}/{camel_name}"); + + quote! { + #code => Some(::pgls_diagnostics::category!(#category_path)) + } + }) + .collect(); + + // Generate match arms for rule metadata lookup by name + let metadata_arms: Vec<_> = rules + .values() + .map(|rule| { + let camel_name = &rule.camel_name; + let code = &rule.code; + let scope = &rule.scope; + let description = &rule.description; + let fixes: Vec<&str> = rule.fixes.iter().map(|s| s.as_str()).collect(); + + quote! { + #camel_name => Some(RuleMetadata { + code: #code, + name: #camel_name, + scope: #scope, + description: #description, + fixes: &[#(#fixes),*], + }) + } + }) + .collect(); + + // Generate match arms for rule metadata lookup by code + let metadata_by_code_arms: Vec<_> = rules + .values() + .map(|rule| { + let camel_name = &rule.camel_name; + let code = &rule.code; + let scope = &rule.scope; + let description = &rule.description; + let fixes: Vec<&str> = rule.fixes.iter().map(|s| s.as_str()).collect(); + + quote! { + #code => Some(RuleMetadata { + code: #code, + name: #camel_name, + scope: #scope, + description: #description, + fixes: &[#(#fixes),*], + }) + } + }) + .collect(); + + let content = quote! { + //! Generated file, do not edit by hand, see `xtask/codegen` + + use pgls_analyse::RegistryVisitor; + use pgls_diagnostics::Category; + + /// Metadata for a pglinter rule + #[derive(Debug, Clone, Copy)] + pub struct RuleMetadata { + /// Rule code (e.g., "B001") + pub code: &'static str, + /// Rule name in camelCase + pub name: &'static str, + /// Rule scope (BASE, SCHEMA, CLUSTER) + pub scope: &'static str, + /// Description of what the rule detects + pub description: &'static str, + /// Suggested fixes + pub fixes: &'static [&'static str], + } + + /// Visit all pglinter rules using the visitor pattern + pub fn visit_registry(registry: &mut V) { + registry.record_category::(); + } + + /// Get the pglinter rule code from the camelCase name + pub fn get_rule_code(name: &str) -> Option<&'static str> { + match name { + #( #code_arms, )* + _ => None, + } + } + + /// Get the diagnostic category for a rule code + pub fn get_rule_category(code: &str) -> Option<&'static Category> { + match code { + #( #category_arms, )* + _ => None, + } + } + + /// Get rule metadata by name (camelCase) + pub fn get_rule_metadata(name: &str) -> Option { + match name { + #( #metadata_arms, )* + _ => None, + } + } + + /// Get rule metadata by code (e.g., "B001", "S001", "C001") + pub fn get_rule_metadata_by_code(code: &str) -> Option { + match code { + #( #metadata_by_code_arms, )* + _ => None, + } + } + }; + + let formatted = xtask::reformat(content)?; + update(®istry_path, &formatted, &Mode::Overwrite)?; + + Ok(()) +} + +/// Update the categories.rs file with pglinter rules +fn update_categories_file(rules: &BTreeMap) -> Result<()> { + let categories_path = + project_root().join("crates/pgls_diagnostics_categories/src/categories.rs"); + + let mut content = fs2::read_to_string(&categories_path)?; + + // Generate pglinter rule entries grouped by category + let mut pglinter_rules: Vec<(String, String)> = rules + .values() + .map(|rule| { + let category = scope_to_category(&rule.scope); + let url = format!( + "https://github.com/pmpetit/pglinter#{}", + rule.code.to_lowercase() + ); + + ( + category.to_string(), + format!( + " \"pglinter/{}/{}\": \"{}\",", + category, rule.camel_name, url + ), + ) + }) + .collect(); + + // Sort by category, then by entry + pglinter_rules.sort_by(|a, b| a.0.cmp(&b.0).then_with(|| a.1.cmp(&b.1))); + + // Add meta diagnostics at the start + let mut all_entries = vec![ + " // Meta diagnostics".to_string(), + " \"pglinter/extensionNotInstalled\": \"Install the pglinter extension with: CREATE EXTENSION pglinter\",".to_string(), + " \"pglinter/ruleDisabledInExtension\": \"Enable the rule in the extension with: UPDATE pglinter.rules SET enable = true WHERE code = ''\",".to_string(), + ]; + + // Add rule categories + let mut current_category = String::new(); + for (category, entry) in &pglinter_rules { + if category != ¤t_category { + current_category = category.clone(); + all_entries.push(format!( + " // {} rules ({}-series)", + Case::Pascal.convert(category), + match category.as_str() { + "base" => "B", + "schema" => "S", + "cluster" => "C", + _ => "?", + } + )); + } + all_entries.push(entry.clone()); + } + + let pglinter_entries = all_entries.join("\n"); + + // Replace content between pglinter rules markers + let rules_start = "// pglinter rules start"; + let rules_end = "// pglinter rules end"; + + content = replace_between_markers( + &content, + rules_start, + rules_end, + &format!("\n{pglinter_entries}\n "), + )?; + + // Generate pglinter group entries + let mut categories: Vec = pglinter_rules.iter().map(|(cat, _)| cat.clone()).collect(); + categories.sort(); + categories.dedup(); + + let mut group_entries = vec![" \"pglinter\",".to_string()]; + for category in categories { + group_entries.push(format!(" \"pglinter/{category}\",")); + } + let groups_content = group_entries.join("\n"); + + // Replace content between pglinter groups markers + let groups_start = "// Pglinter groups start"; + let groups_end = "// Pglinter groups end"; + + content = replace_between_markers( + &content, + groups_start, + groups_end, + &format!("\n{groups_content}\n "), + )?; + + fs2::write(categories_path, content)?; + + Ok(()) +} + +/// Replace content between two markers +fn replace_between_markers( + content: &str, + start_marker: &str, + end_marker: &str, + new_content: &str, +) -> Result { + let start_pos = content + .find(start_marker) + .with_context(|| format!("Could not find '{start_marker}' marker"))?; + + let end_pos = content + .find(end_marker) + .with_context(|| format!("Could not find '{end_marker}' marker"))?; + + let mut result = String::new(); + result.push_str(&content[..start_pos + start_marker.len()]); + result.push_str(new_content); + result.push_str(&content[end_pos..]); + + Ok(result) +} diff --git a/xtask/codegen/src/lib.rs b/xtask/codegen/src/lib.rs index 3ed82ace1..268439e05 100644 --- a/xtask/codegen/src/lib.rs +++ b/xtask/codegen/src/lib.rs @@ -5,6 +5,7 @@ mod generate_bindings; mod generate_configuration; mod generate_crate; mod generate_new_analyser_rule; +mod generate_pglinter; mod generate_splinter; pub use self::generate_analyser::generate_analyser; @@ -12,6 +13,7 @@ pub use self::generate_bindings::generate_bindings; pub use self::generate_configuration::{generate_rules_configuration, generate_tool_configuration}; pub use self::generate_crate::generate_crate; pub use self::generate_new_analyser_rule::generate_new_analyser_rule; +pub use self::generate_pglinter::generate_pglinter; pub use self::generate_splinter::generate_splinter; use bpaf::Bpaf; use generate_new_analyser_rule::Category; @@ -95,4 +97,7 @@ pub enum TaskCommand { /// Generate splinter categories from the SQL file #[bpaf(command)] Splinter, + /// Generate pglinter rules from pglinter_repo/sql/rules.sql + #[bpaf(command)] + Pglinter, } diff --git a/xtask/codegen/src/main.rs b/xtask/codegen/src/main.rs index 43d11b44c..ca425db00 100644 --- a/xtask/codegen/src/main.rs +++ b/xtask/codegen/src/main.rs @@ -3,7 +3,7 @@ use xtask::{project_root, pushd, Result}; use xtask_codegen::{ generate_analyser, generate_bindings, generate_crate, generate_new_analyser_rule, - generate_rules_configuration, generate_splinter, task_command, TaskCommand, + generate_pglinter, generate_rules_configuration, generate_splinter, task_command, TaskCommand, }; fn main() -> Result<()> { @@ -34,6 +34,9 @@ fn main() -> Result<()> { TaskCommand::Splinter => { generate_splinter()?; } + TaskCommand::Pglinter => { + generate_pglinter()?; + } } Ok(()) From 80fe09e65c9e1a95958b51d3cc69f9c1bc1c1d76 Mon Sep 17 00:00:00 2001 From: psteinroe Date: Mon, 29 Dec 2025 15:36:17 +0100 Subject: [PATCH 02/16] fix: Dockerfile --- .github/actions/setup-postgres/action.yml | 39 +++++++++++++++++------ .github/workflows/pull_request.yml | 17 ++++++++-- 2 files changed, 43 insertions(+), 13 deletions(-) diff --git a/.github/actions/setup-postgres/action.yml b/.github/actions/setup-postgres/action.yml index ef54fde2b..15fdb5dd0 100644 --- a/.github/actions/setup-postgres/action.yml +++ b/.github/actions/setup-postgres/action.yml @@ -59,7 +59,7 @@ runs: echo "Extension library files:" ls -la "$(pg_config --pkglibdir)/" | grep plpgsql || echo "No plpgsql_check library found" - # Install the pglinter extension on macOS + # Install the pglinter extension on macOS (pgrx-based Rust extension) - name: Install and compile pglinter if: runner.os == 'macOS' shell: bash @@ -67,16 +67,22 @@ runs: # First, ensure we're using the same PostgreSQL that the action installed export PATH="$(pg_config --bindir):$PATH" + # Install cargo-pgrx (version must match pglinter's pgrx dependency) + cargo install cargo-pgrx --version 0.16.1 --locked + + # Determine postgres version for pgrx init + PG_VERSION=$(pg_config --version | grep -oE '[0-9]+' | head -1) + echo "PostgreSQL version: $PG_VERSION" + + # Initialize pgrx for the installed PostgreSQL version + cargo pgrx init --pg${PG_VERSION} $(which pg_config) + # Clone and build pglinter git clone https://github.com/pmpetit/pglinter.git cd pglinter - # Clean and compile - make USE_PGXS=1 clean - make USE_PGXS=1 all - - # Install (may need sudo depending on permissions) - sudo make USE_PGXS=1 install + # Install using pgrx + cargo pgrx install --pg-config $(which pg_config) --release # Verify installation echo "Extension control files:" @@ -99,17 +105,30 @@ runs: psql -c "SELECT extname, extversion FROM pg_extension WHERE extname IN ('plpgsql_check', 'pglinter');" # For Linux, use custom Docker image with plpgsql_check and pglinter - - name: Build and start PostgreSQL with extensions + - name: Set up Docker Buildx + if: runner.os == 'Linux' + uses: docker/setup-buildx-action@v3 + + - name: Build PostgreSQL image with cache + if: runner.os == 'Linux' + uses: docker/build-push-action@v5 + with: + context: . + load: true + tags: postgres-language-server-dev:latest + cache-from: type=gha + cache-to: type=gha,mode=max + + - name: Start PostgreSQL container if: runner.os == 'Linux' shell: bash run: | - docker build -t postgres-plpgsql-check:latest . docker run -d --name postgres \ -e POSTGRES_USER=postgres \ -e POSTGRES_PASSWORD=postgres \ -e POSTGRES_DB=postgres \ -p 5432:5432 \ - postgres-plpgsql-check:latest + postgres-language-server-dev:latest # Wait for postgres to be ready for _ in {1..30}; do if docker exec postgres pg_isready -U postgres; then diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index 83a963f7d..d66dd5e42 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -96,15 +96,26 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # we need to use the same database as we do locally for sqlx prepare to output the same hashes - - name: Build and start PostgreSQL with plpgsql_check + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Build PostgreSQL image with cache + uses: docker/build-push-action@v5 + with: + context: . + load: true + tags: postgres-language-server-dev:latest + cache-from: type=gha + cache-to: type=gha,mode=max + + - name: Start PostgreSQL run: | - docker build -t postgres-plpgsql-check:latest . docker run -d --name postgres \ -e POSTGRES_USER=postgres \ -e POSTGRES_PASSWORD=postgres \ -e POSTGRES_DB=postgres \ -p 5432:5432 \ - postgres-plpgsql-check:latest + postgres-language-server-dev:latest # Wait for postgres to be ready for _ in {1..30}; do if docker exec postgres pg_isready -U postgres; then From 49ab70e6dd264b8cf8c78b9368e13f33987b8ace Mon Sep 17 00:00:00 2001 From: psteinroe Date: Mon, 29 Dec 2025 15:53:29 +0100 Subject: [PATCH 03/16] fix: clone extensions to /tmp on macOS to avoid workspace conflicts --- .github/actions/setup-postgres/action.yml | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/.github/actions/setup-postgres/action.yml b/.github/actions/setup-postgres/action.yml index 15fdb5dd0..18784210a 100644 --- a/.github/actions/setup-postgres/action.yml +++ b/.github/actions/setup-postgres/action.yml @@ -41,7 +41,8 @@ runs: echo "Extension directory: $(pg_config --sharedir)/extension" echo "Library directory: $(pg_config --pkglibdir)" - # Clone and build plpgsql_check (pinned to v2.7.11 for PG15 compatibility) + # Clone and build plpgsql_check (clone to /tmp to avoid workspace conflicts, pinned to v2.7.11) + cd /tmp git clone --branch v2.7.11 --depth 1 https://github.com/okbob/plpgsql_check.git cd plpgsql_check @@ -77,8 +78,9 @@ runs: # Initialize pgrx for the installed PostgreSQL version cargo pgrx init --pg${PG_VERSION} $(which pg_config) - # Clone and build pglinter - git clone https://github.com/pmpetit/pglinter.git + # Clone and build pglinter (clone to /tmp, use feat/83/violation_list for rule_messages) + cd /tmp + git clone -b feat/83/violation_list https://github.com/pmpetit/pglinter.git cd pglinter # Install using pgrx From 09ba963091cc0cb02d7177ec6efa296ec36cf9e8 Mon Sep 17 00:00:00 2001 From: psteinroe Date: Mon, 29 Dec 2025 16:05:42 +0100 Subject: [PATCH 04/16] fix: explicitly create extensions after container start --- .github/actions/setup-postgres/action.yml | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/.github/actions/setup-postgres/action.yml b/.github/actions/setup-postgres/action.yml index 18784210a..d47366248 100644 --- a/.github/actions/setup-postgres/action.yml +++ b/.github/actions/setup-postgres/action.yml @@ -139,3 +139,14 @@ runs: sleep 1 done + # Verify extensions are created, create if missing + echo "Verifying extensions..." + docker exec postgres psql -U postgres -c "CREATE EXTENSION IF NOT EXISTS plpgsql_check;" + docker exec postgres psql -U postgres -c "CREATE EXTENSION IF NOT EXISTS pglinter;" + + # Show extension status + docker exec postgres psql -U postgres -c "SELECT extname, extversion FROM pg_extension WHERE extname IN ('plpgsql_check', 'pglinter');" + + # Verify pglinter schema exists + docker exec postgres psql -U postgres -c "SELECT schema_name FROM information_schema.schemata WHERE schema_name = 'pglinter';" + From 70c87fb11987822069894f872ff4eae949431ae9 Mon Sep 17 00:00:00 2001 From: psteinroe Date: Mon, 29 Dec 2025 16:18:26 +0100 Subject: [PATCH 05/16] fix: create extensions in template1 for SQLx test databases --- .github/actions/setup-postgres/action.yml | 9 +++++++-- Dockerfile | 15 ++++++++------- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/.github/actions/setup-postgres/action.yml b/.github/actions/setup-postgres/action.yml index d47366248..dade90058 100644 --- a/.github/actions/setup-postgres/action.yml +++ b/.github/actions/setup-postgres/action.yml @@ -139,8 +139,13 @@ runs: sleep 1 done - # Verify extensions are created, create if missing - echo "Verifying extensions..." + # Verify extensions are created in template1 (for SQLx test databases) + echo "Creating extensions in template1..." + docker exec postgres psql -U postgres -d template1 -c "CREATE EXTENSION IF NOT EXISTS plpgsql_check;" + docker exec postgres psql -U postgres -d template1 -c "CREATE EXTENSION IF NOT EXISTS pglinter;" + + # Also create in postgres database + echo "Creating extensions in postgres database..." docker exec postgres psql -U postgres -c "CREATE EXTENSION IF NOT EXISTS plpgsql_check;" docker exec postgres psql -U postgres -c "CREATE EXTENSION IF NOT EXISTS pglinter;" diff --git a/Dockerfile b/Dockerfile index a26c74eb3..17ce1c073 100644 --- a/Dockerfile +++ b/Dockerfile @@ -31,10 +31,11 @@ RUN apt-get update && \ rm -rf /var/lib/apt/lists/* # Add initialization script for extensions -# Only create in postgres database (NOT template1) to avoid polluting test databases -# Tests that need extensions can create them explicitly -RUN printf '%s\n' \ - "CREATE SCHEMA IF NOT EXISTS extensions;" \ - "CREATE EXTENSION IF NOT EXISTS plpgsql_check SCHEMA extensions;" \ - "CREATE EXTENSION IF NOT EXISTS pglinter SCHEMA extensions;" \ - > /docker-entrypoint-initdb.d/01-create-extension.sql +# Create extensions in template1 so they're available in all new databases (for SQLx tests) +# Also create in postgres database for direct connections +RUN echo "\\c template1" > /docker-entrypoint-initdb.d/01-create-extension.sql && \ + echo "CREATE EXTENSION IF NOT EXISTS plpgsql_check;" >> /docker-entrypoint-initdb.d/01-create-extension.sql && \ + echo "CREATE EXTENSION IF NOT EXISTS pglinter;" >> /docker-entrypoint-initdb.d/01-create-extension.sql && \ + echo "\\c postgres" >> /docker-entrypoint-initdb.d/01-create-extension.sql && \ + echo "CREATE EXTENSION IF NOT EXISTS plpgsql_check;" >> /docker-entrypoint-initdb.d/01-create-extension.sql && \ + echo "CREATE EXTENSION IF NOT EXISTS pglinter;" >> /docker-entrypoint-initdb.d/01-create-extension.sql From 5ae4f0518d39b5e6a9a47f6050af2b02c924f18e Mon Sep 17 00:00:00 2001 From: psteinroe Date: Mon, 29 Dec 2025 16:27:37 +0100 Subject: [PATCH 06/16] fix: install extensions in 'extensions' schema to avoid lint warnings --- .github/actions/setup-postgres/action.yml | 11 +++++++---- Dockerfile | 13 ++++++++----- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/.github/actions/setup-postgres/action.yml b/.github/actions/setup-postgres/action.yml index dade90058..433e70d8e 100644 --- a/.github/actions/setup-postgres/action.yml +++ b/.github/actions/setup-postgres/action.yml @@ -140,14 +140,17 @@ runs: done # Verify extensions are created in template1 (for SQLx test databases) + # Use 'extensions' schema to avoid triggering extensionInPublic lint echo "Creating extensions in template1..." - docker exec postgres psql -U postgres -d template1 -c "CREATE EXTENSION IF NOT EXISTS plpgsql_check;" - docker exec postgres psql -U postgres -d template1 -c "CREATE EXTENSION IF NOT EXISTS pglinter;" + docker exec postgres psql -U postgres -d template1 -c "CREATE SCHEMA IF NOT EXISTS extensions;" + docker exec postgres psql -U postgres -d template1 -c "CREATE EXTENSION IF NOT EXISTS plpgsql_check SCHEMA extensions;" + docker exec postgres psql -U postgres -d template1 -c "CREATE EXTENSION IF NOT EXISTS pglinter SCHEMA extensions;" # Also create in postgres database echo "Creating extensions in postgres database..." - docker exec postgres psql -U postgres -c "CREATE EXTENSION IF NOT EXISTS plpgsql_check;" - docker exec postgres psql -U postgres -c "CREATE EXTENSION IF NOT EXISTS pglinter;" + docker exec postgres psql -U postgres -c "CREATE SCHEMA IF NOT EXISTS extensions;" + docker exec postgres psql -U postgres -c "CREATE EXTENSION IF NOT EXISTS plpgsql_check SCHEMA extensions;" + docker exec postgres psql -U postgres -c "CREATE EXTENSION IF NOT EXISTS pglinter SCHEMA extensions;" # Show extension status docker exec postgres psql -U postgres -c "SELECT extname, extversion FROM pg_extension WHERE extname IN ('plpgsql_check', 'pglinter');" diff --git a/Dockerfile b/Dockerfile index 17ce1c073..94b1a1e0c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -31,11 +31,14 @@ RUN apt-get update && \ rm -rf /var/lib/apt/lists/* # Add initialization script for extensions -# Create extensions in template1 so they're available in all new databases (for SQLx tests) +# Create extensions in a dedicated 'extensions' schema to avoid triggering extensionInPublic lint +# Create in template1 so they're available in all new databases (for SQLx tests) # Also create in postgres database for direct connections RUN echo "\\c template1" > /docker-entrypoint-initdb.d/01-create-extension.sql && \ - echo "CREATE EXTENSION IF NOT EXISTS plpgsql_check;" >> /docker-entrypoint-initdb.d/01-create-extension.sql && \ - echo "CREATE EXTENSION IF NOT EXISTS pglinter;" >> /docker-entrypoint-initdb.d/01-create-extension.sql && \ + echo "CREATE SCHEMA IF NOT EXISTS extensions;" >> /docker-entrypoint-initdb.d/01-create-extension.sql && \ + echo "CREATE EXTENSION IF NOT EXISTS plpgsql_check SCHEMA extensions;" >> /docker-entrypoint-initdb.d/01-create-extension.sql && \ + echo "CREATE EXTENSION IF NOT EXISTS pglinter SCHEMA extensions;" >> /docker-entrypoint-initdb.d/01-create-extension.sql && \ echo "\\c postgres" >> /docker-entrypoint-initdb.d/01-create-extension.sql && \ - echo "CREATE EXTENSION IF NOT EXISTS plpgsql_check;" >> /docker-entrypoint-initdb.d/01-create-extension.sql && \ - echo "CREATE EXTENSION IF NOT EXISTS pglinter;" >> /docker-entrypoint-initdb.d/01-create-extension.sql + echo "CREATE SCHEMA IF NOT EXISTS extensions;" >> /docker-entrypoint-initdb.d/01-create-extension.sql && \ + echo "CREATE EXTENSION IF NOT EXISTS plpgsql_check SCHEMA extensions;" >> /docker-entrypoint-initdb.d/01-create-extension.sql && \ + echo "CREATE EXTENSION IF NOT EXISTS pglinter SCHEMA extensions;" >> /docker-entrypoint-initdb.d/01-create-extension.sql From b850fb1f4cc57ecd9ddae5a8ff40d2708787f119 Mon Sep 17 00:00:00 2001 From: psteinroe Date: Tue, 30 Dec 2025 08:58:07 +0100 Subject: [PATCH 07/16] fix: ci --- .github/actions/setup-postgres/action.yml | 13 +- ...test_helper__completes_quoted_columns.snap | 1 + ...completes_quoted_columns_with_aliases.snap | 1 + ...oes_not_complete_cols_in_join_clauses.snap | 1 + ...__test_helper__handles_nested_queries.snap | 1 + ...t_helper__ignores_cols_in_from_clause.snap | 1 + ...__prefers_columns_of_mentioned_tables.snap | 1 + ...helper__prefers_not_mentioned_columns.snap | 1 + ...iple_columns_if_no_relation_specified.snap | 1 + ...columns_in_alter_table_and_drop_table.snap | 1 + ...er__suggests_columns_in_insert_clause.snap | 1 + ...per__suggests_columns_in_where_clause.snap | 1 + ..._suggests_columns_policy_using_clause.snap | 1 + ...ests_relevant_columns_without_letters.snap | 1 + crates/pgls_pglinter/src/cache.rs | 10 +- crates/pgls_pglinter/src/diagnostics.rs | 90 +++------ crates/pgls_pglinter/src/lib.rs | 32 +--- crates/pgls_pglinter/src/sarif.rs | 172 ------------------ crates/pgls_pglinter/tests/diagnostics.rs | 99 +++++++--- .../tests/snapshots/fk_without_index.snap | 13 ++ .../tests/snapshots/multiple_issues.snap | 35 ++++ .../snapshots/objects_with_uppercase.snap | 13 ++ .../snapshots/table_with_primary_key.snap | 13 ++ .../snapshots/table_without_primary_key.snap | 13 ++ 24 files changed, 214 insertions(+), 302 deletions(-) delete mode 100644 crates/pgls_pglinter/src/sarif.rs create mode 100644 crates/pgls_pglinter/tests/snapshots/fk_without_index.snap create mode 100644 crates/pgls_pglinter/tests/snapshots/multiple_issues.snap create mode 100644 crates/pgls_pglinter/tests/snapshots/objects_with_uppercase.snap create mode 100644 crates/pgls_pglinter/tests/snapshots/table_with_primary_key.snap create mode 100644 crates/pgls_pglinter/tests/snapshots/table_without_primary_key.snap diff --git a/.github/actions/setup-postgres/action.yml b/.github/actions/setup-postgres/action.yml index 433e70d8e..4289f1d2a 100644 --- a/.github/actions/setup-postgres/action.yml +++ b/.github/actions/setup-postgres/action.yml @@ -139,14 +139,8 @@ runs: sleep 1 done - # Verify extensions are created in template1 (for SQLx test databases) - # Use 'extensions' schema to avoid triggering extensionInPublic lint - echo "Creating extensions in template1..." - docker exec postgres psql -U postgres -d template1 -c "CREATE SCHEMA IF NOT EXISTS extensions;" - docker exec postgres psql -U postgres -d template1 -c "CREATE EXTENSION IF NOT EXISTS plpgsql_check SCHEMA extensions;" - docker exec postgres psql -U postgres -d template1 -c "CREATE EXTENSION IF NOT EXISTS pglinter SCHEMA extensions;" - - # Also create in postgres database + # Create extensions in postgres database only (NOT template1) + # This avoids polluting test databases - tests that need extensions can create them explicitly echo "Creating extensions in postgres database..." docker exec postgres psql -U postgres -c "CREATE SCHEMA IF NOT EXISTS extensions;" docker exec postgres psql -U postgres -c "CREATE EXTENSION IF NOT EXISTS plpgsql_check SCHEMA extensions;" @@ -155,6 +149,3 @@ runs: # Show extension status docker exec postgres psql -U postgres -c "SELECT extname, extversion FROM pg_extension WHERE extname IN ('plpgsql_check', 'pglinter');" - # Verify pglinter schema exists - docker exec postgres psql -U postgres -c "SELECT schema_name FROM information_schema.schemata WHERE schema_name = 'pglinter';" - diff --git a/crates/pgls_completions/src/snapshots/pgls_completions__test_helper__completes_quoted_columns.snap b/crates/pgls_completions/src/snapshots/pgls_completions__test_helper__completes_quoted_columns.snap index 490bec446..9d0831d42 100644 --- a/crates/pgls_completions/src/snapshots/pgls_completions__test_helper__completes_quoted_columns.snap +++ b/crates/pgls_completions/src/snapshots/pgls_completions__test_helper__completes_quoted_columns.snap @@ -1,6 +1,7 @@ --- source: crates/pgls_completions/src/test_helper.rs expression: final_snapshot +snapshot_kind: text --- ***Setup*** diff --git a/crates/pgls_completions/src/snapshots/pgls_completions__test_helper__completes_quoted_columns_with_aliases.snap b/crates/pgls_completions/src/snapshots/pgls_completions__test_helper__completes_quoted_columns_with_aliases.snap index 580ca1614..3f95b16bb 100644 --- a/crates/pgls_completions/src/snapshots/pgls_completions__test_helper__completes_quoted_columns_with_aliases.snap +++ b/crates/pgls_completions/src/snapshots/pgls_completions__test_helper__completes_quoted_columns_with_aliases.snap @@ -1,6 +1,7 @@ --- source: crates/pgls_completions/src/test_helper.rs expression: final_snapshot +snapshot_kind: text --- ***Setup*** diff --git a/crates/pgls_completions/src/snapshots/pgls_completions__test_helper__does_not_complete_cols_in_join_clauses.snap b/crates/pgls_completions/src/snapshots/pgls_completions__test_helper__does_not_complete_cols_in_join_clauses.snap index 3a6dd9965..87e7d9986 100644 --- a/crates/pgls_completions/src/snapshots/pgls_completions__test_helper__does_not_complete_cols_in_join_clauses.snap +++ b/crates/pgls_completions/src/snapshots/pgls_completions__test_helper__does_not_complete_cols_in_join_clauses.snap @@ -1,6 +1,7 @@ --- source: crates/pgls_completions/src/test_helper.rs expression: final_snapshot +snapshot_kind: text --- ***Setup*** diff --git a/crates/pgls_completions/src/snapshots/pgls_completions__test_helper__handles_nested_queries.snap b/crates/pgls_completions/src/snapshots/pgls_completions__test_helper__handles_nested_queries.snap index 0d203a4c1..4b2df051d 100644 --- a/crates/pgls_completions/src/snapshots/pgls_completions__test_helper__handles_nested_queries.snap +++ b/crates/pgls_completions/src/snapshots/pgls_completions__test_helper__handles_nested_queries.snap @@ -1,6 +1,7 @@ --- source: crates/pgls_completions/src/test_helper.rs expression: final_snapshot +snapshot_kind: text --- ***Setup*** diff --git a/crates/pgls_completions/src/snapshots/pgls_completions__test_helper__ignores_cols_in_from_clause.snap b/crates/pgls_completions/src/snapshots/pgls_completions__test_helper__ignores_cols_in_from_clause.snap index 3c16b91c7..dbcd0251b 100644 --- a/crates/pgls_completions/src/snapshots/pgls_completions__test_helper__ignores_cols_in_from_clause.snap +++ b/crates/pgls_completions/src/snapshots/pgls_completions__test_helper__ignores_cols_in_from_clause.snap @@ -1,6 +1,7 @@ --- source: crates/pgls_completions/src/test_helper.rs expression: final_snapshot +snapshot_kind: text --- ***Setup*** diff --git a/crates/pgls_completions/src/snapshots/pgls_completions__test_helper__prefers_columns_of_mentioned_tables.snap b/crates/pgls_completions/src/snapshots/pgls_completions__test_helper__prefers_columns_of_mentioned_tables.snap index ca024d628..cd43623d7 100644 --- a/crates/pgls_completions/src/snapshots/pgls_completions__test_helper__prefers_columns_of_mentioned_tables.snap +++ b/crates/pgls_completions/src/snapshots/pgls_completions__test_helper__prefers_columns_of_mentioned_tables.snap @@ -1,6 +1,7 @@ --- source: crates/pgls_completions/src/test_helper.rs expression: final_snapshot +snapshot_kind: text --- ***Setup*** diff --git a/crates/pgls_completions/src/snapshots/pgls_completions__test_helper__prefers_not_mentioned_columns.snap b/crates/pgls_completions/src/snapshots/pgls_completions__test_helper__prefers_not_mentioned_columns.snap index ab52b99cd..7c15b168d 100644 --- a/crates/pgls_completions/src/snapshots/pgls_completions__test_helper__prefers_not_mentioned_columns.snap +++ b/crates/pgls_completions/src/snapshots/pgls_completions__test_helper__prefers_not_mentioned_columns.snap @@ -1,6 +1,7 @@ --- source: crates/pgls_completions/src/test_helper.rs expression: final_snapshot +snapshot_kind: text --- ***Setup*** diff --git a/crates/pgls_completions/src/snapshots/pgls_completions__test_helper__shows_multiple_columns_if_no_relation_specified.snap b/crates/pgls_completions/src/snapshots/pgls_completions__test_helper__shows_multiple_columns_if_no_relation_specified.snap index 6defa0825..d1be0aa89 100644 --- a/crates/pgls_completions/src/snapshots/pgls_completions__test_helper__shows_multiple_columns_if_no_relation_specified.snap +++ b/crates/pgls_completions/src/snapshots/pgls_completions__test_helper__shows_multiple_columns_if_no_relation_specified.snap @@ -1,6 +1,7 @@ --- source: crates/pgls_completions/src/test_helper.rs expression: final_snapshot +snapshot_kind: text --- ***Setup*** diff --git a/crates/pgls_completions/src/snapshots/pgls_completions__test_helper__suggests_columns_in_alter_table_and_drop_table.snap b/crates/pgls_completions/src/snapshots/pgls_completions__test_helper__suggests_columns_in_alter_table_and_drop_table.snap index 11ff82787..bc5ab8921 100644 --- a/crates/pgls_completions/src/snapshots/pgls_completions__test_helper__suggests_columns_in_alter_table_and_drop_table.snap +++ b/crates/pgls_completions/src/snapshots/pgls_completions__test_helper__suggests_columns_in_alter_table_and_drop_table.snap @@ -1,6 +1,7 @@ --- source: crates/pgls_completions/src/test_helper.rs expression: final_snapshot +snapshot_kind: text --- ***Setup*** diff --git a/crates/pgls_completions/src/snapshots/pgls_completions__test_helper__suggests_columns_in_insert_clause.snap b/crates/pgls_completions/src/snapshots/pgls_completions__test_helper__suggests_columns_in_insert_clause.snap index 0323de0f3..8518a68b0 100644 --- a/crates/pgls_completions/src/snapshots/pgls_completions__test_helper__suggests_columns_in_insert_clause.snap +++ b/crates/pgls_completions/src/snapshots/pgls_completions__test_helper__suggests_columns_in_insert_clause.snap @@ -1,6 +1,7 @@ --- source: crates/pgls_completions/src/test_helper.rs expression: final_snapshot +snapshot_kind: text --- ***Setup*** diff --git a/crates/pgls_completions/src/snapshots/pgls_completions__test_helper__suggests_columns_in_where_clause.snap b/crates/pgls_completions/src/snapshots/pgls_completions__test_helper__suggests_columns_in_where_clause.snap index a4f591bc0..43894e393 100644 --- a/crates/pgls_completions/src/snapshots/pgls_completions__test_helper__suggests_columns_in_where_clause.snap +++ b/crates/pgls_completions/src/snapshots/pgls_completions__test_helper__suggests_columns_in_where_clause.snap @@ -1,6 +1,7 @@ --- source: crates/pgls_completions/src/test_helper.rs expression: final_snapshot +snapshot_kind: text --- ***Setup*** diff --git a/crates/pgls_completions/src/snapshots/pgls_completions__test_helper__suggests_columns_policy_using_clause.snap b/crates/pgls_completions/src/snapshots/pgls_completions__test_helper__suggests_columns_policy_using_clause.snap index d7441a1ef..73757937b 100644 --- a/crates/pgls_completions/src/snapshots/pgls_completions__test_helper__suggests_columns_policy_using_clause.snap +++ b/crates/pgls_completions/src/snapshots/pgls_completions__test_helper__suggests_columns_policy_using_clause.snap @@ -1,6 +1,7 @@ --- source: crates/pgls_completions/src/test_helper.rs expression: final_snapshot +snapshot_kind: text --- ***Setup*** diff --git a/crates/pgls_completions/src/snapshots/pgls_completions__test_helper__suggests_relevant_columns_without_letters.snap b/crates/pgls_completions/src/snapshots/pgls_completions__test_helper__suggests_relevant_columns_without_letters.snap index 4ee4242ae..8343f5af2 100644 --- a/crates/pgls_completions/src/snapshots/pgls_completions__test_helper__suggests_relevant_columns_without_letters.snap +++ b/crates/pgls_completions/src/snapshots/pgls_completions__test_helper__suggests_relevant_columns_without_letters.snap @@ -1,6 +1,7 @@ --- source: crates/pgls_completions/src/test_helper.rs expression: final_snapshot +snapshot_kind: text --- ***Setup*** diff --git a/crates/pgls_pglinter/src/cache.rs b/crates/pgls_pglinter/src/cache.rs index 015fd73f2..f8739e4d5 100644 --- a/crates/pgls_pglinter/src/cache.rs +++ b/crates/pgls_pglinter/src/cache.rs @@ -43,12 +43,12 @@ impl PglinterCache { } } -/// Get disabled rules using pglinter's official API: pglinter.show_rules() +/// Get disabled rules by querying the pglinter.rules table +/// Uses the rules table directly since show_rules() only outputs to NOTICE pub async fn get_disabled_rules(conn: &PgPool) -> Result, sqlx::Error> { - let rows: Vec<(String, bool)> = - sqlx::query_as("SELECT rule_code, enabled FROM pglinter.show_rules()") - .fetch_all(conn) - .await?; + let rows: Vec<(String, bool)> = sqlx::query_as("SELECT code, enable FROM pglinter.rules") + .fetch_all(conn) + .await?; Ok(rows .into_iter() diff --git a/crates/pgls_pglinter/src/diagnostics.rs b/crates/pgls_pglinter/src/diagnostics.rs index ad19c81fc..eb75026c6 100644 --- a/crates/pgls_pglinter/src/diagnostics.rs +++ b/crates/pgls_pglinter/src/diagnostics.rs @@ -1,4 +1,4 @@ -//! Pglinter diagnostic types and conversion from SARIF +//! Pglinter diagnostic types use pgls_diagnostics::{ Advices, Category, DatabaseObjectOwned, Diagnostic, LogCategory, MessageAndDescription, @@ -6,8 +6,6 @@ use pgls_diagnostics::{ }; use std::io; -use crate::sarif; - /// A specialized diagnostic for pglinter (database-level linting via pglinter extension). #[derive(Debug, Diagnostic, PartialEq)] pub struct PglinterDiagnostic { @@ -75,72 +73,7 @@ impl Advices for PglinterAdvices { } } -/// Error when converting SARIF to diagnostics -#[derive(Debug)] -pub struct UnknownRuleError { - pub rule_code: String, -} - -impl std::fmt::Display for UnknownRuleError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "Unknown pglinter rule code: {}", self.rule_code) - } -} - -impl std::error::Error for UnknownRuleError {} - impl PglinterDiagnostic { - /// Try to convert a single SARIF result to a pglinter diagnostic - pub fn try_from_sarif( - result: &sarif::Result, - rule_code: &str, - ) -> Result { - let category = - crate::registry::get_rule_category(rule_code).ok_or_else(|| UnknownRuleError { - rule_code: rule_code.to_string(), - })?; - - let metadata = crate::registry::get_rule_metadata_by_code(rule_code); - - let severity = match result.level_str() { - "error" => Severity::Error, - "warning" => Severity::Warning, - "note" => Severity::Information, - _ => Severity::Warning, - }; - - let message = result.message_text().to_string(); - let description = metadata - .map(|m| m.description.to_string()) - .unwrap_or_else(|| message.clone()); - - let fixes = metadata - .map(|m| m.fixes.iter().map(|s| s.to_string()).collect()) - .unwrap_or_default(); - - let object_list = { - let names = result.logical_location_names(); - if names.is_empty() { - None - } else { - Some(names.join("\n")) - } - }; - - Ok(PglinterDiagnostic { - category, - db_object: None, - message: message.into(), - severity, - advices: PglinterAdvices { - description, - rule_code: Some(rule_code.to_string()), - fixes, - object_list, - }, - }) - } - /// Create diagnostic for missing pglinter extension pub fn extension_not_installed() -> PglinterDiagnostic { PglinterDiagnostic { @@ -180,4 +113,25 @@ impl PglinterDiagnostic { }, } } + + /// Create diagnostic from rule code using known metadata + pub fn from_rule_code(rule_code: &str) -> Option { + let category = crate::registry::get_rule_category(rule_code)?; + let metadata = crate::registry::get_rule_metadata_by_code(rule_code)?; + + let fixes: Vec = metadata.fixes.iter().map(|s| s.to_string()).collect(); + + Some(PglinterDiagnostic { + category, + db_object: None, + message: metadata.description.into(), + severity: Severity::Warning, + advices: PglinterAdvices { + description: metadata.description.to_string(), + rule_code: Some(rule_code.to_string()), + fixes, + object_list: None, + }, + }) + } } diff --git a/crates/pgls_pglinter/src/lib.rs b/crates/pgls_pglinter/src/lib.rs index 8506a70e2..870a29072 100644 --- a/crates/pgls_pglinter/src/lib.rs +++ b/crates/pgls_pglinter/src/lib.rs @@ -5,7 +5,6 @@ mod diagnostics; pub mod registry; pub mod rule; pub mod rules; -pub mod sarif; use pgls_analyse::{AnalysisFilter, RegistryVisitor, RuleMeta}; use pgls_schema_cache::SchemaCache; @@ -14,7 +13,6 @@ use sqlx::PgPool; pub use cache::PglinterCache; pub use diagnostics::{PglinterAdvices, PglinterDiagnostic}; pub use rule::PglinterRule; -pub use sarif::SarifLog; /// Parameters for running pglinter #[derive(Debug)] @@ -121,37 +119,25 @@ pub async fn run_pglinter( Ok(results) } -/// Execute a single pglinter rule using pglinter.check_rule() +/// Execute a single pglinter rule using pglinter.check(rule_code) +/// Returns true if the rule detected issues async fn execute_rule( conn: &PgPool, rule_code: &str, ) -> Result>, sqlx::Error> { - let result: Option = sqlx::query_scalar("SELECT pglinter.check_rule($1)") + let has_issues: bool = sqlx::query_scalar("SELECT pglinter.check($1)") .bind(rule_code) - .fetch_optional(conn) + .fetch_one(conn) .await?; - let Some(sarif_json) = result else { - return Ok(None); - }; - - let sarif = match SarifLog::parse(&sarif_json) { - Ok(s) => s, - Err(_) => return Ok(None), - }; - - if !sarif.has_results() { + if !has_issues { return Ok(None); } - let diags: Vec<_> = sarif - .all_results() - .filter_map(|result| PglinterDiagnostic::try_from_sarif(result, rule_code).ok()) - .collect(); - - if diags.is_empty() { - Ok(None) + // Rule fired - create diagnostic from our known metadata + if let Some(diag) = PglinterDiagnostic::from_rule_code(rule_code) { + Ok(Some(vec![diag])) } else { - Ok(Some(diags)) + Ok(None) } } diff --git a/crates/pgls_pglinter/src/sarif.rs b/crates/pgls_pglinter/src/sarif.rs deleted file mode 100644 index d57d0979f..000000000 --- a/crates/pgls_pglinter/src/sarif.rs +++ /dev/null @@ -1,172 +0,0 @@ -//! Generic SARIF (Static Analysis Results Interchange Format) parser -//! -//! SARIF is a standard format for static analysis tool output. -//! See: https://sarifweb.azurewebsites.net/ - -use serde::Deserialize; - -/// SARIF 2.1.0 root object -#[derive(Debug, Deserialize)] -pub struct SarifLog { - #[serde(default)] - pub runs: Vec, -} - -/// A single run of a static analysis tool -#[derive(Debug, Deserialize)] -pub struct Run { - #[serde(default)] - pub results: Vec, - pub tool: Option, -} - -/// Information about the tool that produced the results -#[derive(Debug, Deserialize)] -pub struct Tool { - pub driver: Option, -} - -/// The tool driver (main component) -#[derive(Debug, Deserialize)] -pub struct Driver { - pub name: Option, - pub version: Option, -} - -/// A single result from the analysis -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct Result { - /// The rule ID that was violated - pub rule_id: Option, - /// Severity level: "error", "warning", "note", "none" - pub level: Option, - /// The result message - pub message: Option, - /// Locations where the issue was found - #[serde(default)] - pub locations: Vec, -} - -/// A message with text content -#[derive(Debug, Deserialize)] -pub struct Message { - pub text: Option, -} - -/// A location in the source -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct Location { - pub physical_location: Option, - pub logical_locations: Option>, -} - -/// A physical location (file, line, column) -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct PhysicalLocation { - pub artifact_location: Option, - pub region: Option, -} - -/// Location of an artifact (file) -#[derive(Debug, Deserialize)] -pub struct ArtifactLocation { - pub uri: Option, -} - -/// A region within a file -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct Region { - pub start_line: Option, - pub start_column: Option, - pub end_line: Option, - pub end_column: Option, -} - -/// A logical location (schema, table, function name, etc.) -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct LogicalLocation { - pub name: Option, - pub fully_qualified_name: Option, - pub kind: Option, -} - -impl SarifLog { - /// Parse SARIF JSON into a structured log - pub fn parse(json: &str) -> std::result::Result { - serde_json::from_str(json) - } - - /// Get all results from all runs - pub fn all_results(&self) -> impl Iterator { - self.runs.iter().flat_map(|run| run.results.iter()) - } - - /// Check if there are any results - pub fn has_results(&self) -> bool { - self.runs.iter().any(|run| !run.results.is_empty()) - } -} - -impl Result { - /// Get the severity level, defaulting to "warning" - pub fn level_str(&self) -> &str { - self.level.as_deref().unwrap_or("warning") - } - - /// Get the message text, defaulting to empty string - pub fn message_text(&self) -> &str { - self.message - .as_ref() - .and_then(|m| m.text.as_deref()) - .unwrap_or("") - } - - /// Get logical location names (e.g., affected database objects) - pub fn logical_location_names(&self) -> Vec<&str> { - self.locations - .iter() - .filter_map(|loc| loc.logical_locations.as_ref()) - .flatten() - .filter_map(|ll| ll.fully_qualified_name.as_deref().or(ll.name.as_deref())) - .collect() - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_parse_minimal_sarif() { - let json = r#"{ - "runs": [{ - "results": [{ - "ruleId": "B001", - "level": "warning", - "message": { "text": "Table without primary key" } - }] - }] - }"#; - - let log = SarifLog::parse(json).unwrap(); - assert!(log.has_results()); - - let results: Vec<_> = log.all_results().collect(); - assert_eq!(results.len(), 1); - assert_eq!(results[0].rule_id.as_deref(), Some("B001")); - assert_eq!(results[0].level_str(), "warning"); - assert_eq!(results[0].message_text(), "Table without primary key"); - } - - #[test] - fn test_parse_empty_sarif() { - let json = r#"{"runs": [{"results": []}]}"#; - let log = SarifLog::parse(json).unwrap(); - assert!(!log.has_results()); - } -} diff --git a/crates/pgls_pglinter/tests/diagnostics.rs b/crates/pgls_pglinter/tests/diagnostics.rs index abdd68bcd..6d37418c2 100644 --- a/crates/pgls_pglinter/tests/diagnostics.rs +++ b/crates/pgls_pglinter/tests/diagnostics.rs @@ -1,6 +1,6 @@ //! Integration tests for pglinter diagnostics //! -//! These tests require the pglinter extension to be installed in the test database. +//! These tests configure pglinter thresholds to 0% so rules fire deterministically. use pgls_analyse::AnalysisFilter; use pgls_console::fmt::{Formatter, HTML}; @@ -49,42 +49,63 @@ impl Visit for TestVisitor { } } +/// Configure pglinter for deterministic testing: +/// - Set all thresholds to 0% warning, 1% error so any violation triggers +/// - Disable cluster-level rules that depend on pg_hba.conf +async fn configure_pglinter_for_tests(pool: &PgPool) { + // Set thresholds to 0% warning for deterministic behavior + let rules_to_configure = [ + "B001", "B002", "B003", "B004", "B005", "B006", "B007", "B008", "B009", "B010", "B011", + "B012", "S001", "S002", "S003", "S004", "S005", + ]; + + for rule in rules_to_configure { + let _ = sqlx::query("SELECT pglinter.update_rule_levels($1, 0, 1)") + .bind(rule) + .execute(pool) + .await; + } + + // Disable cluster-level rules (depend on pg_hba.conf, not deterministic) + for rule in ["C001", "C002", "C003"] { + let _ = sqlx::query("SELECT pglinter.disable_rule($1)") + .bind(rule) + .execute(pool) + .await; + } +} + struct TestSetup<'a> { name: &'a str, setup: &'a str, test_db: &'a PgPool, + /// Only include rules matching these prefixes (e.g., ["B001", "B005"]) + /// Empty means include all non-cluster rules + rule_filter: Vec<&'a str>, } impl TestSetup<'_> { async fn test(self) { - // Load schema cache - let schema_cache = SchemaCache::load(self.test_db) + sqlx::raw_sql("CREATE EXTENSION IF NOT EXISTS pglinter") + .execute(self.test_db) .await - .expect("Failed to load schema cache"); + .expect("pglinter extension not available"); - // Assert pglinter extension is installed - assert!( - schema_cache.extensions.iter().any(|e| e.name == "pglinter"), - "pglinter extension must be installed for tests to run" - ); + configure_pglinter_for_tests(self.test_db).await; - // Run setup SQL sqlx::raw_sql(self.setup) .execute(self.test_db) .await .expect("Failed to setup test database"); - // Reload schema cache after setup let schema_cache = SchemaCache::load(self.test_db) .await - .expect("Failed to reload schema cache"); + .expect("Failed to load schema cache"); - // Load pglinter cache let cache = PglinterCache::load(self.test_db, &schema_cache) .await .expect("Failed to load pglinter cache"); - // Run pglinter checks with all rules enabled let filter = AnalysisFilter::default(); let diagnostics = run_pglinter( PglinterParams { @@ -97,26 +118,49 @@ impl TestSetup<'_> { .await .expect("Failed to run pglinter checks"); - let content = if diagnostics.is_empty() { + // Filter diagnostics + let filtered: Vec<_> = diagnostics + .iter() + .filter(|d| { + let category = d.category().map(|c| c.name()).unwrap_or(""); + // Exclude cluster-level rules + if category.contains("/cluster/") { + return false; + } + // Apply rule filter if specified + if !self.rule_filter.is_empty() { + let rule_code = d + .advices + .rule_code + .as_ref() + .map(|s| s.as_str()) + .unwrap_or(""); + return self.rule_filter.iter().any(|f| rule_code == *f); + } + true + }) + .collect(); + + // Sort by category for deterministic output + let mut sorted = filtered; + sorted.sort_by_key(|d| d.category().map(|c| c.name()).unwrap_or("unknown")); + + let content = if sorted.is_empty() { String::from("No Diagnostics") } else { let mut result = String::new(); - for (idx, diagnostic) in diagnostics.iter().enumerate() { + for (idx, diagnostic) in sorted.iter().enumerate() { if idx > 0 { writeln!(&mut result).unwrap(); writeln!(&mut result, "---").unwrap(); writeln!(&mut result).unwrap(); } - // Write category let category_name = diagnostic.category().map(|c| c.name()).unwrap_or("unknown"); writeln!(&mut result, "Category: {category_name}").unwrap(); - - // Write severity writeln!(&mut result, "Severity: {:?}", diagnostic.severity()).unwrap(); - // Write message let mut msg_content = vec![]; let mut writer = HTML::new(&mut msg_content); let mut formatter = Formatter::new(&mut writer); @@ -128,7 +172,6 @@ impl TestSetup<'_> { ) .unwrap(); - // Write advices using custom visitor let mut visitor = TestVisitor::new(); diagnostic.advices(&mut visitor).unwrap(); let advice_text = visitor.into_string(); @@ -148,16 +191,21 @@ impl TestSetup<'_> { } } -/// Test that checks extension availability +/// Test that pglinter extension can be created #[sqlx::test(migrator = "pgls_test_utils::MIGRATIONS")] async fn extension_check(test_db: PgPool) { + sqlx::raw_sql("CREATE EXTENSION IF NOT EXISTS pglinter") + .execute(&test_db) + .await + .expect("pglinter extension not available"); + let schema_cache = SchemaCache::load(&test_db) .await .expect("Failed to load schema cache"); assert!( schema_cache.extensions.iter().any(|e| e.name == "pglinter"), - "pglinter extension must be installed for tests to run" + "pglinter extension not found" ); } @@ -173,6 +221,7 @@ async fn table_without_primary_key(test_db: PgPool) { ); "#, test_db: &test_db, + rule_filter: vec!["B001"], } .test() .await; @@ -190,6 +239,7 @@ async fn table_with_primary_key(test_db: PgPool) { ); "#, test_db: &test_db, + rule_filter: vec!["B001"], } .test() .await; @@ -207,6 +257,7 @@ async fn objects_with_uppercase(test_db: PgPool) { ); "#, test_db: &test_db, + rule_filter: vec!["B005"], } .test() .await; @@ -229,6 +280,7 @@ async fn fk_without_index(test_db: PgPool) { ); "#, test_db: &test_db, + rule_filter: vec!["B003"], } .test() .await; @@ -261,6 +313,7 @@ async fn multiple_issues(test_db: PgPool) { ); "#, test_db: &test_db, + rule_filter: vec!["B001", "B003", "B005"], } .test() .await; diff --git a/crates/pgls_pglinter/tests/snapshots/fk_without_index.snap b/crates/pgls_pglinter/tests/snapshots/fk_without_index.snap new file mode 100644 index 000000000..5f79a0dea --- /dev/null +++ b/crates/pgls_pglinter/tests/snapshots/fk_without_index.snap @@ -0,0 +1,13 @@ +--- +source: crates/pgls_pglinter/tests/diagnostics.rs +expression: content +snapshot_kind: text +--- +Category: pglinter/base/howManyTableWithoutIndexOnFk +Severity: Warning +Message: Count number of tables without index on foreign key. +Advices: +Count number of tables without index on foreign key. +[Info] Rule: B003 +How to fix: +[Info] 1. create a index on foreign key or change warning/error threshold diff --git a/crates/pgls_pglinter/tests/snapshots/multiple_issues.snap b/crates/pgls_pglinter/tests/snapshots/multiple_issues.snap new file mode 100644 index 000000000..15ed63c69 --- /dev/null +++ b/crates/pgls_pglinter/tests/snapshots/multiple_issues.snap @@ -0,0 +1,35 @@ +--- +source: crates/pgls_pglinter/tests/diagnostics.rs +expression: content +snapshot_kind: text +--- +Category: pglinter/base/howManyObjectsWithUppercase +Severity: Warning +Message: Count number of objects with uppercase in name or in columns. +Advices: +Count number of objects with uppercase in name or in columns. +[Info] Rule: B005 +How to fix: +[Info] 1. Do not use uppercase for any database objects + +--- + +Category: pglinter/base/howManyTableWithoutIndexOnFk +Severity: Warning +Message: Count number of tables without index on foreign key. +Advices: +Count number of tables without index on foreign key. +[Info] Rule: B003 +How to fix: +[Info] 1. create a index on foreign key or change warning/error threshold + +--- + +Category: pglinter/base/howManyTableWithoutPrimaryKey +Severity: Warning +Message: Count number of tables without primary key. +Advices: +Count number of tables without primary key. +[Info] Rule: B001 +How to fix: +[Info] 1. create a primary key or change warning/error threshold diff --git a/crates/pgls_pglinter/tests/snapshots/objects_with_uppercase.snap b/crates/pgls_pglinter/tests/snapshots/objects_with_uppercase.snap new file mode 100644 index 000000000..3fbd674a6 --- /dev/null +++ b/crates/pgls_pglinter/tests/snapshots/objects_with_uppercase.snap @@ -0,0 +1,13 @@ +--- +source: crates/pgls_pglinter/tests/diagnostics.rs +expression: content +snapshot_kind: text +--- +Category: pglinter/base/howManyObjectsWithUppercase +Severity: Warning +Message: Count number of objects with uppercase in name or in columns. +Advices: +Count number of objects with uppercase in name or in columns. +[Info] Rule: B005 +How to fix: +[Info] 1. Do not use uppercase for any database objects diff --git a/crates/pgls_pglinter/tests/snapshots/table_with_primary_key.snap b/crates/pgls_pglinter/tests/snapshots/table_with_primary_key.snap new file mode 100644 index 000000000..b6611adb5 --- /dev/null +++ b/crates/pgls_pglinter/tests/snapshots/table_with_primary_key.snap @@ -0,0 +1,13 @@ +--- +source: crates/pgls_pglinter/tests/diagnostics.rs +expression: content +snapshot_kind: text +--- +Category: pglinter/base/howManyTableWithoutPrimaryKey +Severity: Warning +Message: Count number of tables without primary key. +Advices: +Count number of tables without primary key. +[Info] Rule: B001 +How to fix: +[Info] 1. create a primary key or change warning/error threshold diff --git a/crates/pgls_pglinter/tests/snapshots/table_without_primary_key.snap b/crates/pgls_pglinter/tests/snapshots/table_without_primary_key.snap new file mode 100644 index 000000000..b6611adb5 --- /dev/null +++ b/crates/pgls_pglinter/tests/snapshots/table_without_primary_key.snap @@ -0,0 +1,13 @@ +--- +source: crates/pgls_pglinter/tests/diagnostics.rs +expression: content +snapshot_kind: text +--- +Category: pglinter/base/howManyTableWithoutPrimaryKey +Severity: Warning +Message: Count number of tables without primary key. +Advices: +Count number of tables without primary key. +[Info] Rule: B001 +How to fix: +[Info] 1. create a primary key or change warning/error threshold From 1f084fa10a46f5465715e8a5b0fae2581bdee1de Mon Sep 17 00:00:00 2001 From: psteinroe Date: Tue, 30 Dec 2025 09:17:53 +0100 Subject: [PATCH 08/16] fix: skip pglinter tests on Windows and fix clippy warnings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add #![cfg(not(target_os = "windows"))] to skip pglinter tests on Windows since the pglinter extension is not available there (only Linux/macOS) - Fix clippy warnings: use as_deref() and contains() instead of manual patterns - Remove table_with_primary_key test since pglinter checks all tables globally, making a "no diagnostics for table with PK" test impossible when other tables exist - Add plpgsql_check as dependency in test setup 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- Cargo.toml | 1 + crates/pgls_pglinter/tests/diagnostics.rs | 46 +++++++++---------- .../snapshots/table_with_primary_key.snap | 13 ------ 3 files changed, 22 insertions(+), 38 deletions(-) delete mode 100644 crates/pgls_pglinter/tests/snapshots/table_with_primary_key.snap diff --git a/Cargo.toml b/Cargo.toml index bdfbe4887..252cf5aa6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -80,6 +80,7 @@ pgls_lexer = { path = "./crates/pgls_lexer", version = "0.0.0" pgls_lexer_codegen = { path = "./crates/pgls_lexer_codegen", version = "0.0.0" } pgls_lsp = { path = "./crates/pgls_lsp", version = "0.0.0" } pgls_markup = { path = "./crates/pgls_markup", version = "0.0.0" } +pgls_matcher = { path = "./crates/pgls_matcher", version = "0.0.0" } pgls_pglinter = { path = "./crates/pgls_pglinter", version = "0.0.0" } pgls_plpgsql_check = { path = "./crates/pgls_plpgsql_check", version = "0.0.0" } pgls_pretty_print = { path = "./crates/pgls_pretty_print", version = "0.0.0" } diff --git a/crates/pgls_pglinter/tests/diagnostics.rs b/crates/pgls_pglinter/tests/diagnostics.rs index 6d37418c2..971678b8a 100644 --- a/crates/pgls_pglinter/tests/diagnostics.rs +++ b/crates/pgls_pglinter/tests/diagnostics.rs @@ -1,6 +1,11 @@ //! Integration tests for pglinter diagnostics //! //! These tests configure pglinter thresholds to 0% so rules fire deterministically. +//! +//! Note: These tests require the pglinter extension to be installed, which is only +//! available on Linux (via Docker) and macOS. Windows CI does not have pglinter. + +#![cfg(not(target_os = "windows"))] use pgls_analyse::AnalysisFilter; use pgls_console::fmt::{Formatter, HTML}; @@ -86,6 +91,12 @@ struct TestSetup<'a> { impl TestSetup<'_> { async fn test(self) { + // Create required extensions (pglinter may depend on plpgsql_check) + sqlx::raw_sql("CREATE EXTENSION IF NOT EXISTS plpgsql_check") + .execute(self.test_db) + .await + .expect("plpgsql_check extension not available"); + sqlx::raw_sql("CREATE EXTENSION IF NOT EXISTS pglinter") .execute(self.test_db) .await @@ -129,13 +140,8 @@ impl TestSetup<'_> { } // Apply rule filter if specified if !self.rule_filter.is_empty() { - let rule_code = d - .advices - .rule_code - .as_ref() - .map(|s| s.as_str()) - .unwrap_or(""); - return self.rule_filter.iter().any(|f| rule_code == *f); + let rule_code = d.advices.rule_code.as_deref().unwrap_or(""); + return self.rule_filter.contains(&rule_code); } true }) @@ -194,6 +200,12 @@ impl TestSetup<'_> { /// Test that pglinter extension can be created #[sqlx::test(migrator = "pgls_test_utils::MIGRATIONS")] async fn extension_check(test_db: PgPool) { + // Create required extensions (pglinter may depend on plpgsql_check) + sqlx::raw_sql("CREATE EXTENSION IF NOT EXISTS plpgsql_check") + .execute(&test_db) + .await + .expect("plpgsql_check extension not available"); + sqlx::raw_sql("CREATE EXTENSION IF NOT EXISTS pglinter") .execute(&test_db) .await @@ -210,6 +222,8 @@ async fn extension_check(test_db: PgPool) { } /// Test B001: Table without primary key +/// Note: pglinter checks ALL tables in the database globally, not just specific tables. +/// So this test verifies that B001 fires when any table lacks a primary key. #[sqlx::test(migrator = "pgls_test_utils::MIGRATIONS")] async fn table_without_primary_key(test_db: PgPool) { TestSetup { @@ -227,24 +241,6 @@ async fn table_without_primary_key(test_db: PgPool) { .await; } -/// Test with a clean table (has primary key) -#[sqlx::test(migrator = "pgls_test_utils::MIGRATIONS")] -async fn table_with_primary_key(test_db: PgPool) { - TestSetup { - name: "table_with_primary_key", - setup: r#" - CREATE TABLE public.test_with_pk ( - id serial PRIMARY KEY, - name text - ); - "#, - test_db: &test_db, - rule_filter: vec!["B001"], - } - .test() - .await; -} - /// Test B005: Objects with uppercase names #[sqlx::test(migrator = "pgls_test_utils::MIGRATIONS")] async fn objects_with_uppercase(test_db: PgPool) { diff --git a/crates/pgls_pglinter/tests/snapshots/table_with_primary_key.snap b/crates/pgls_pglinter/tests/snapshots/table_with_primary_key.snap deleted file mode 100644 index b6611adb5..000000000 --- a/crates/pgls_pglinter/tests/snapshots/table_with_primary_key.snap +++ /dev/null @@ -1,13 +0,0 @@ ---- -source: crates/pgls_pglinter/tests/diagnostics.rs -expression: content -snapshot_kind: text ---- -Category: pglinter/base/howManyTableWithoutPrimaryKey -Severity: Warning -Message: Count number of tables without primary key. -Advices: -Count number of tables without primary key. -[Info] Rule: B001 -How to fix: -[Info] 1. create a primary key or change warning/error threshold From 23b5e9c2c2436507bd68765ccddf9a760f47b57c Mon Sep 17 00:00:00 2001 From: psteinroe Date: Sun, 11 Jan 2026 16:12:14 +0100 Subject: [PATCH 09/16] chore: update lockfile --- Cargo.lock | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index 1fc0daaff..f8091f5f7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2752,6 +2752,7 @@ dependencies = [ "pgls_console", "pgls_diagnostics", "pgls_env", + "pgls_matcher", "pgls_text_size", "rustc-hash 2.1.0", "schemars", @@ -2921,6 +2922,15 @@ dependencies = [ "quote", ] +[[package]] +name = "pgls_matcher" +version = "0.0.0" +dependencies = [ + "pgls_console", + "pgls_diagnostics", + "rustc-hash 2.1.0", +] + [[package]] name = "pgls_pglinter" version = "0.0.0" @@ -3017,8 +3027,10 @@ version = "0.0.0" dependencies = [ "insta", "pgls_analyse", + "pgls_configuration", "pgls_console", "pgls_diagnostics", + "pgls_matcher", "pgls_schema_cache", "pgls_test_utils", "serde", @@ -3177,6 +3189,7 @@ dependencies = [ "pgls_fs", "pgls_hover", "pgls_lexer", + "pgls_matcher", "pgls_plpgsql_check", "pgls_query", "pgls_query_ext", From 7a08213aff3328a68e0c13ffc48ba6a68dfe7271 Mon Sep 17 00:00:00 2001 From: psteinroe Date: Wed, 14 Jan 2026 10:30:28 +0100 Subject: [PATCH 10/16] progress --- ...def7cbde2c9e609d34c1bfcb87f50ed0f25ff.json | 32 ++++ ...c98d80b50a02c30a54b30611ca40b99d51ab7.json | 38 +++++ crates/pgls_pglinter/src/diagnostics.rs | 114 +++++++++++++ crates/pgls_pglinter/src/lib.rs | 156 +++++++++++++++--- .../tests/snapshots/fk_without_index.snap | 4 +- .../tests/snapshots/multiple_issues.snap | 100 ++++++++++- .../snapshots/objects_with_uppercase.snap | 59 ++++++- .../snapshots/table_without_primary_key.snap | 4 +- crates/pgls_schema_cache/src/indexes.rs | 21 +++ crates/pgls_schema_cache/src/lib.rs | 4 + .../pgls_schema_cache/src/queries/indexes.sql | 11 ++ .../src/queries/sequences.sql | 8 + crates/pgls_schema_cache/src/schema_cache.rs | 30 ++++ crates/pgls_schema_cache/src/sequences.rs | 20 +++ 14 files changed, 566 insertions(+), 35 deletions(-) create mode 100644 .sqlx/query-0aba89d3e0ed2e4586b94ea8fe2def7cbde2c9e609d34c1bfcb87f50ed0f25ff.json create mode 100644 .sqlx/query-4f7d0241b0c52b2d6742b441e9ac98d80b50a02c30a54b30611ca40b99d51ab7.json create mode 100644 crates/pgls_schema_cache/src/indexes.rs create mode 100644 crates/pgls_schema_cache/src/queries/indexes.sql create mode 100644 crates/pgls_schema_cache/src/queries/sequences.sql create mode 100644 crates/pgls_schema_cache/src/sequences.rs diff --git a/.sqlx/query-0aba89d3e0ed2e4586b94ea8fe2def7cbde2c9e609d34c1bfcb87f50ed0f25ff.json b/.sqlx/query-0aba89d3e0ed2e4586b94ea8fe2def7cbde2c9e609d34c1bfcb87f50ed0f25ff.json new file mode 100644 index 000000000..18290122d --- /dev/null +++ b/.sqlx/query-0aba89d3e0ed2e4586b94ea8fe2def7cbde2c9e609d34c1bfcb87f50ed0f25ff.json @@ -0,0 +1,32 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT\n c.oid::bigint as \"id!\",\n n.nspname as \"schema!\",\n c.relname as \"name!\"\nFROM pg_catalog.pg_class c\nJOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace\nWHERE c.relkind = 'S'\n AND n.nspname NOT IN ('pg_catalog', 'information_schema', 'pg_toast')\n", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id!", + "type_info": "Int8" + }, + { + "ordinal": 1, + "name": "schema!", + "type_info": "Name" + }, + { + "ordinal": 2, + "name": "name!", + "type_info": "Name" + } + ], + "parameters": { + "Left": [] + }, + "nullable": [ + null, + false, + false + ] + }, + "hash": "0aba89d3e0ed2e4586b94ea8fe2def7cbde2c9e609d34c1bfcb87f50ed0f25ff" +} diff --git a/.sqlx/query-4f7d0241b0c52b2d6742b441e9ac98d80b50a02c30a54b30611ca40b99d51ab7.json b/.sqlx/query-4f7d0241b0c52b2d6742b441e9ac98d80b50a02c30a54b30611ca40b99d51ab7.json new file mode 100644 index 000000000..daa11c2c1 --- /dev/null +++ b/.sqlx/query-4f7d0241b0c52b2d6742b441e9ac98d80b50a02c30a54b30611ca40b99d51ab7.json @@ -0,0 +1,38 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT\n c.oid::bigint as \"id!\",\n n.nspname as \"schema!\",\n c.relname as \"name!\",\n t.relname as \"table_name!\"\nFROM pg_catalog.pg_class c\nJOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace\nJOIN pg_catalog.pg_index i ON i.indexrelid = c.oid\nJOIN pg_catalog.pg_class t ON t.oid = i.indrelid\nWHERE c.relkind = 'i'\n AND n.nspname NOT IN ('pg_catalog', 'information_schema', 'pg_toast')\n", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id!", + "type_info": "Int8" + }, + { + "ordinal": 1, + "name": "schema!", + "type_info": "Name" + }, + { + "ordinal": 2, + "name": "name!", + "type_info": "Name" + }, + { + "ordinal": 3, + "name": "table_name!", + "type_info": "Name" + } + ], + "parameters": { + "Left": [] + }, + "nullable": [ + null, + false, + false, + false + ] + }, + "hash": "4f7d0241b0c52b2d6742b441e9ac98d80b50a02c30a54b30611ca40b99d51ab7" +} diff --git a/crates/pgls_pglinter/src/diagnostics.rs b/crates/pgls_pglinter/src/diagnostics.rs index eb75026c6..47753951f 100644 --- a/crates/pgls_pglinter/src/diagnostics.rs +++ b/crates/pgls_pglinter/src/diagnostics.rs @@ -134,4 +134,118 @@ impl PglinterDiagnostic { }, }) } + + /// Create diagnostic from a pglinter violation with optional object info + pub fn from_violation( + rule_code: &str, + db_object: Option, + ) -> Option { + let category = crate::registry::get_rule_category(rule_code)?; + let metadata = crate::registry::get_rule_metadata_by_code(rule_code)?; + + let fixes: Vec = metadata.fixes.iter().map(|s| s.to_string()).collect(); + + // Generate a violation-specific message + let message = violation_message(rule_code, db_object.as_ref()); + + // Generate a violation-specific advice (more detailed explanation) + let advice_description = violation_advice(rule_code); + + Some(PglinterDiagnostic { + category, + db_object, + message: message.into(), + severity: Severity::Warning, + advices: PglinterAdvices { + description: advice_description, + rule_code: Some(rule_code.to_string()), + fixes, + object_list: None, + }, + }) + } +} + +/// Generate a user-friendly violation message for a specific object +fn violation_message(rule_code: &str, db_object: Option<&DatabaseObjectOwned>) -> String { + let obj_name = db_object + .map(|obj| { + if let Some(ref schema) = obj.schema { + format!("'{}.{}'", schema, obj.name) + } else { + format!("'{}'", obj.name) + } + }) + .unwrap_or_else(|| "Object".to_string()); + + let obj_type = db_object + .and_then(|obj| obj.object_type.as_deref()) + .unwrap_or("object"); + + match rule_code { + // Base rules + "B001" => format!("Table {obj_name} has no primary key"), + "B002" => format!("Index on {obj_name} is redundant"), + "B003" => format!("Foreign key on {obj_name} has no index"), + "B004" => format!("Index on {obj_name} is unused"), + "B005" => format!( + "{} {} uses uppercase characters", + capitalize(obj_type), + obj_name + ), + "B006" => format!("Table {obj_name} is never selected from"), + "B007" => format!("Foreign key on {obj_name} references table outside its schema"), + "B008" => format!("Foreign key on {obj_name} has type mismatch"), + "B009" => format!("Table {obj_name} has duplicate trigger"), + "B010" => format!( + "{} {} uses reserved SQL keyword", + capitalize(obj_type), + obj_name + ), + "B011" => format!("Tables in {obj_name} have different owners"), + "B012" => format!("Table {obj_name} has composite primary key with too many columns"), + // Schema rules + "S001" => format!("Schema {obj_name} has no default role granted"), + "S002" => format!("Schema {obj_name} name is prefixed/suffixed with environment"), + "S003" => format!("Schema {obj_name} has insecure public access"), + "S004" => format!("Schema {obj_name} owner is an internal role"), + "S005" => format!("Schema {obj_name} owner doesn't match table owners"), + // Cluster rules + "C001" | "C002" | "C003" => "Cluster configuration issue".to_string(), + // Fallback + _ => format!("{} {} has a violation", capitalize(obj_type), obj_name), + } +} + +fn capitalize(s: &str) -> String { + let mut c = s.chars(); + match c.next() { + None => String::new(), + Some(f) => f.to_uppercase().collect::() + c.as_str(), + } +} + +/// Generate detailed advice for a specific rule violation +fn violation_advice(rule_code: &str) -> String { + match rule_code { + "B001" => "Tables without primary keys cannot be uniquely identified, which causes issues with replication, foreign keys, and efficient updates/deletes.".to_string(), + "B002" => "Redundant indexes waste storage space and slow down write operations without providing query benefits.".to_string(), + "B003" => "Foreign keys without indexes cause slow cascading operations and inefficient join queries.".to_string(), + "B004" => "Unused indexes consume storage and slow down writes without benefiting any queries.".to_string(), + "B005" => "Using uppercase in identifiers requires quoting and can cause case-sensitivity issues.".to_string(), + "B006" => "Tables never queried may be obsolete and candidates for removal.".to_string(), + "B007" => "Cross-schema foreign keys can cause issues with schema-level operations and access control.".to_string(), + "B008" => "Type mismatches in foreign keys can cause implicit casts, affecting performance and data integrity.".to_string(), + "B009" => "Duplicate triggers may cause unexpected behavior or redundant processing.".to_string(), + "B010" => "Using SQL reserved keywords as identifiers requires quoting and may cause compatibility issues.".to_string(), + "B011" => "Mixed ownership in schemas can cause permission and maintenance issues.".to_string(), + "B012" => "Large composite primary keys are inefficient for indexing and foreign key references.".to_string(), + "S001" => "Schemas without default role grants may have inconsistent permission patterns.".to_string(), + "S002" => "Environment prefixes/suffixes in schema names indicate environment-specific configuration that should be handled differently.".to_string(), + "S003" => "Insecure public access to schemas can expose data to unauthorized users.".to_string(), + "S004" => "Internal role ownership of schemas can cause maintenance and security issues.".to_string(), + "S005" => "Mismatched schema/table ownership can cause permission inconsistencies.".to_string(), + "C001" | "C002" | "C003" => "Cluster configuration issues may affect database stability and performance.".to_string(), + _ => String::new(), + } } diff --git a/crates/pgls_pglinter/src/lib.rs b/crates/pgls_pglinter/src/lib.rs index 870a29072..95b3e0542 100644 --- a/crates/pgls_pglinter/src/lib.rs +++ b/crates/pgls_pglinter/src/lib.rs @@ -7,6 +7,7 @@ pub mod rule; pub mod rules; use pgls_analyse::{AnalysisFilter, RegistryVisitor, RuleMeta}; +use pgls_diagnostics::DatabaseObjectOwned; use pgls_schema_cache::SchemaCache; use sqlx::PgPool; @@ -14,6 +15,24 @@ pub use cache::PglinterCache; pub use diagnostics::{PglinterAdvices, PglinterDiagnostic}; pub use rule::PglinterRule; +/// PostgreSQL catalog OIDs for different object types +mod pg_catalog { + pub const PG_CLASS: i64 = 1259; // tables, views, indexes, sequences + pub const PG_PROC: i64 = 1255; // functions, procedures + pub const PG_TYPE: i64 = 1247; // types + pub const PG_NAMESPACE: i64 = 2615; // schemas + pub const PG_ATTRIBUTE: i64 = 1249; // columns (objid=table oid, objsubid=column number) +} + +/// A violation row returned by pglinter.get_violations() +#[derive(Debug, sqlx::FromRow)] +struct ViolationRow { + rule_code: String, + classid: i64, + objid: i64, + objsubid: i32, +} + /// Parameters for running pglinter #[derive(Debug)] pub struct PglinterParams<'a> { @@ -109,35 +128,126 @@ pub async fn run_pglinter( return Ok(results); } - // Execute each rule - for rule_code in &runnable_rules { - if let Some(diags) = execute_rule(params.conn, rule_code).await? { - results.extend(diags); + // Fetch all violations in one query + let violations = fetch_violations(params.conn).await?; + + // Process violations, filtering by enabled rules and resolving objects from cache + for violation in violations { + // Skip violations for rules we're not checking + if !runnable_rules.contains(&violation.rule_code) { + continue; + } + + // Resolve the object from the schema cache + let db_object = resolve_object_from_cache( + params.schema_cache, + violation.classid, + violation.objid, + violation.objsubid, + ); + + // Create a diagnostic for this violation + if let Some(diag) = PglinterDiagnostic::from_violation(&violation.rule_code, db_object) { + results.push(diag); } } Ok(results) } -/// Execute a single pglinter rule using pglinter.check(rule_code) -/// Returns true if the rule detected issues -async fn execute_rule( - conn: &PgPool, - rule_code: &str, -) -> Result>, sqlx::Error> { - let has_issues: bool = sqlx::query_scalar("SELECT pglinter.check($1)") - .bind(rule_code) - .fetch_one(conn) - .await?; - - if !has_issues { - return Ok(None); - } +/// Fetch all violations from pglinter.get_violations() +async fn fetch_violations(conn: &PgPool) -> Result, sqlx::Error> { + sqlx::query_as::<_, ViolationRow>( + "select rule_code, classid::bigint, objid::bigint, objsubid from pglinter.get_violations()", + ) + .fetch_all(conn) + .await +} - // Rule fired - create diagnostic from our known metadata - if let Some(diag) = PglinterDiagnostic::from_rule_code(rule_code) { - Ok(Some(vec![diag])) - } else { - Ok(None) +/// Resolve a Postgres object from the schema cache using its catalog OIDs +fn resolve_object_from_cache( + schema_cache: &SchemaCache, + classid: i64, + objid: i64, + objsubid: i32, +) -> Option { + match classid { + pg_catalog::PG_CLASS => { + // pg_class contains tables, views, indexes, sequences, etc. + // Try tables first, then indexes, then sequences + schema_cache + .find_table_by_id(objid) + .map(|t| DatabaseObjectOwned { + schema: Some(t.schema.clone()), + name: t.name.clone(), + object_type: Some(format!("{:?}", t.table_kind).to_lowercase()), + }) + .or_else(|| { + schema_cache + .find_index_by_id(objid) + .map(|i| DatabaseObjectOwned { + schema: Some(i.schema.clone()), + name: i.name.clone(), + object_type: Some("index".to_string()), + }) + }) + .or_else(|| { + schema_cache + .find_sequence_by_id(objid) + .map(|s| DatabaseObjectOwned { + schema: Some(s.schema.clone()), + name: s.name.clone(), + object_type: Some("sequence".to_string()), + }) + }) + } + pg_catalog::PG_PROC => { + // Functions and procedures + schema_cache + .find_function_by_id(objid) + .map(|f| DatabaseObjectOwned { + schema: Some(f.schema.clone()), + name: f.name.clone(), + object_type: Some(format!("{:?}", f.kind).to_lowercase()), + }) + } + pg_catalog::PG_TYPE => { + // Types + schema_cache + .find_type_by_id(objid) + .map(|t| DatabaseObjectOwned { + schema: Some(t.schema.clone()), + name: t.name.clone(), + object_type: Some("type".to_string()), + }) + } + pg_catalog::PG_NAMESPACE => { + // Schemas + schema_cache + .find_schema_by_id(objid) + .map(|s| DatabaseObjectOwned { + schema: None, + name: s.name.clone(), + object_type: Some("schema".to_string()), + }) + } + pg_catalog::PG_ATTRIBUTE => { + // Columns: objid is table OID, objsubid is column number (attnum) + // Find the column by table OID and column number + let col_num = i64::from(objsubid); + schema_cache + .columns + .iter() + .find(|c| c.table_oid == objid && c.number == col_num) + .map(|c| DatabaseObjectOwned { + schema: Some(c.schema_name.clone()), + name: format!("{}.{}", c.table_name, c.name), + object_type: Some("column".to_string()), + }) + } + _ => { + // Unknown catalog - we can't resolve this object from the cache + None + } } } diff --git a/crates/pgls_pglinter/tests/snapshots/fk_without_index.snap b/crates/pgls_pglinter/tests/snapshots/fk_without_index.snap index 5f79a0dea..ea6afe546 100644 --- a/crates/pgls_pglinter/tests/snapshots/fk_without_index.snap +++ b/crates/pgls_pglinter/tests/snapshots/fk_without_index.snap @@ -5,9 +5,9 @@ snapshot_kind: text --- Category: pglinter/base/howManyTableWithoutIndexOnFk Severity: Warning -Message: Count number of tables without index on foreign key. +Message: Foreign key on Object has no index Advices: -Count number of tables without index on foreign key. +Foreign keys without indexes cause slow cascading operations and inefficient join queries. [Info] Rule: B003 How to fix: [Info] 1. create a index on foreign key or change warning/error threshold diff --git a/crates/pgls_pglinter/tests/snapshots/multiple_issues.snap b/crates/pgls_pglinter/tests/snapshots/multiple_issues.snap index 15ed63c69..2c803dff6 100644 --- a/crates/pgls_pglinter/tests/snapshots/multiple_issues.snap +++ b/crates/pgls_pglinter/tests/snapshots/multiple_issues.snap @@ -5,9 +5,97 @@ snapshot_kind: text --- Category: pglinter/base/howManyObjectsWithUppercase Severity: Warning -Message: Count number of objects with uppercase in name or in columns. +Message: Sequence 'public.BadName_id_seq' uses uppercase characters Advices: -Count number of objects with uppercase in name or in columns. +Using uppercase in identifiers requires quoting and can cause case-sensitivity issues. +[Info] Rule: B005 +How to fix: +[Info] 1. Do not use uppercase for any database objects + +--- + +Category: pglinter/base/howManyObjectsWithUppercase +Severity: Warning +Message: Ordinary 'public.BadName' uses uppercase characters +Advices: +Using uppercase in identifiers requires quoting and can cause case-sensitivity issues. +[Info] Rule: B005 +How to fix: +[Info] 1. Do not use uppercase for any database objects + +--- + +Category: pglinter/base/howManyObjectsWithUppercase +Severity: Warning +Message: Index 'public.BadName_pkey' uses uppercase characters +Advices: +Using uppercase in identifiers requires quoting and can cause case-sensitivity issues. +[Info] Rule: B005 +How to fix: +[Info] 1. Do not use uppercase for any database objects + +--- + +Category: pglinter/base/howManyObjectsWithUppercase +Severity: Warning +Message: Index 'public.BadName_pkey' uses uppercase characters +Advices: +Using uppercase in identifiers requires quoting and can cause case-sensitivity issues. +[Info] Rule: B005 +How to fix: +[Info] 1. Do not use uppercase for any database objects + +--- + +Category: pglinter/base/howManyObjectsWithUppercase +Severity: Warning +Message: Sequence 'public.BadName_id_seq' uses uppercase characters +Advices: +Using uppercase in identifiers requires quoting and can cause case-sensitivity issues. +[Info] Rule: B005 +How to fix: +[Info] 1. Do not use uppercase for any database objects + +--- + +Category: pglinter/base/howManyObjectsWithUppercase +Severity: Warning +Message: Object Object uses uppercase characters +Advices: +Using uppercase in identifiers requires quoting and can cause case-sensitivity issues. +[Info] Rule: B005 +How to fix: +[Info] 1. Do not use uppercase for any database objects + +--- + +Category: pglinter/base/howManyObjectsWithUppercase +Severity: Warning +Message: Object Object uses uppercase characters +Advices: +Using uppercase in identifiers requires quoting and can cause case-sensitivity issues. +[Info] Rule: B005 +How to fix: +[Info] 1. Do not use uppercase for any database objects + +--- + +Category: pglinter/base/howManyObjectsWithUppercase +Severity: Warning +Message: Object Object uses uppercase characters +Advices: +Using uppercase in identifiers requires quoting and can cause case-sensitivity issues. +[Info] Rule: B005 +How to fix: +[Info] 1. Do not use uppercase for any database objects + +--- + +Category: pglinter/base/howManyObjectsWithUppercase +Severity: Warning +Message: Object Object uses uppercase characters +Advices: +Using uppercase in identifiers requires quoting and can cause case-sensitivity issues. [Info] Rule: B005 How to fix: [Info] 1. Do not use uppercase for any database objects @@ -16,9 +104,9 @@ How to fix: Category: pglinter/base/howManyTableWithoutIndexOnFk Severity: Warning -Message: Count number of tables without index on foreign key. +Message: Foreign key on Object has no index Advices: -Count number of tables without index on foreign key. +Foreign keys without indexes cause slow cascading operations and inefficient join queries. [Info] Rule: B003 How to fix: [Info] 1. create a index on foreign key or change warning/error threshold @@ -27,9 +115,9 @@ How to fix: Category: pglinter/base/howManyTableWithoutPrimaryKey Severity: Warning -Message: Count number of tables without primary key. +Message: Table 'public.no_pk' has no primary key Advices: -Count number of tables without primary key. +Tables without primary keys cannot be uniquely identified, which causes issues with replication, foreign keys, and efficient updates/deletes. [Info] Rule: B001 How to fix: [Info] 1. create a primary key or change warning/error threshold diff --git a/crates/pgls_pglinter/tests/snapshots/objects_with_uppercase.snap b/crates/pgls_pglinter/tests/snapshots/objects_with_uppercase.snap index 3fbd674a6..68d4d88cd 100644 --- a/crates/pgls_pglinter/tests/snapshots/objects_with_uppercase.snap +++ b/crates/pgls_pglinter/tests/snapshots/objects_with_uppercase.snap @@ -5,9 +5,64 @@ snapshot_kind: text --- Category: pglinter/base/howManyObjectsWithUppercase Severity: Warning -Message: Count number of objects with uppercase in name or in columns. +Message: Sequence 'public.TestTable_id_seq' uses uppercase characters Advices: -Count number of objects with uppercase in name or in columns. +Using uppercase in identifiers requires quoting and can cause case-sensitivity issues. +[Info] Rule: B005 +How to fix: +[Info] 1. Do not use uppercase for any database objects + +--- + +Category: pglinter/base/howManyObjectsWithUppercase +Severity: Warning +Message: Ordinary 'public.TestTable' uses uppercase characters +Advices: +Using uppercase in identifiers requires quoting and can cause case-sensitivity issues. +[Info] Rule: B005 +How to fix: +[Info] 1. Do not use uppercase for any database objects + +--- + +Category: pglinter/base/howManyObjectsWithUppercase +Severity: Warning +Message: Index 'public.TestTable_pkey' uses uppercase characters +Advices: +Using uppercase in identifiers requires quoting and can cause case-sensitivity issues. +[Info] Rule: B005 +How to fix: +[Info] 1. Do not use uppercase for any database objects + +--- + +Category: pglinter/base/howManyObjectsWithUppercase +Severity: Warning +Message: Column 'public.TestTable.UserName' uses uppercase characters +Advices: +Using uppercase in identifiers requires quoting and can cause case-sensitivity issues. +[Info] Rule: B005 +How to fix: +[Info] 1. Do not use uppercase for any database objects + +--- + +Category: pglinter/base/howManyObjectsWithUppercase +Severity: Warning +Message: Index 'public.TestTable_pkey' uses uppercase characters +Advices: +Using uppercase in identifiers requires quoting and can cause case-sensitivity issues. +[Info] Rule: B005 +How to fix: +[Info] 1. Do not use uppercase for any database objects + +--- + +Category: pglinter/base/howManyObjectsWithUppercase +Severity: Warning +Message: Sequence 'public.TestTable_id_seq' uses uppercase characters +Advices: +Using uppercase in identifiers requires quoting and can cause case-sensitivity issues. [Info] Rule: B005 How to fix: [Info] 1. Do not use uppercase for any database objects diff --git a/crates/pgls_pglinter/tests/snapshots/table_without_primary_key.snap b/crates/pgls_pglinter/tests/snapshots/table_without_primary_key.snap index b6611adb5..5de8cc4e0 100644 --- a/crates/pgls_pglinter/tests/snapshots/table_without_primary_key.snap +++ b/crates/pgls_pglinter/tests/snapshots/table_without_primary_key.snap @@ -5,9 +5,9 @@ snapshot_kind: text --- Category: pglinter/base/howManyTableWithoutPrimaryKey Severity: Warning -Message: Count number of tables without primary key. +Message: Table 'public.test_no_pk' has no primary key Advices: -Count number of tables without primary key. +Tables without primary keys cannot be uniquely identified, which causes issues with replication, foreign keys, and efficient updates/deletes. [Info] Rule: B001 How to fix: [Info] 1. create a primary key or change warning/error threshold diff --git a/crates/pgls_schema_cache/src/indexes.rs b/crates/pgls_schema_cache/src/indexes.rs new file mode 100644 index 000000000..fee160f5e --- /dev/null +++ b/crates/pgls_schema_cache/src/indexes.rs @@ -0,0 +1,21 @@ +use sqlx::PgPool; + +use crate::schema_cache::SchemaCacheItem; + +#[derive(Debug, Default, PartialEq, Eq)] +pub struct Index { + pub id: i64, + pub schema: String, + pub name: String, + pub table_name: String, +} + +impl SchemaCacheItem for Index { + type Item = Index; + + async fn load(pool: &PgPool) -> Result, sqlx::Error> { + sqlx::query_file_as!(Index, "src/queries/indexes.sql") + .fetch_all(pool) + .await + } +} diff --git a/crates/pgls_schema_cache/src/lib.rs b/crates/pgls_schema_cache/src/lib.rs index 6440cd01a..7cdfc7808 100644 --- a/crates/pgls_schema_cache/src/lib.rs +++ b/crates/pgls_schema_cache/src/lib.rs @@ -5,10 +5,12 @@ mod columns; mod extensions; mod functions; +mod indexes; mod policies; mod roles; mod schema_cache; mod schemas; +mod sequences; mod tables; mod triggers; mod types; @@ -17,10 +19,12 @@ mod versions; pub use columns::*; pub use extensions::Extension; pub use functions::{Behavior, Function, FunctionArg, FunctionArgs, ProcKind}; +pub use indexes::Index; pub use policies::{Policy, PolicyCommand}; pub use roles::*; pub use schema_cache::SchemaCache; pub use schemas::Schema; +pub use sequences::Sequence; pub use tables::{ReplicaIdentity, Table, TableKind}; pub use triggers::{Trigger, TriggerAffected, TriggerEvent}; pub use types::{PostgresType, PostgresTypeAttribute}; diff --git a/crates/pgls_schema_cache/src/queries/indexes.sql b/crates/pgls_schema_cache/src/queries/indexes.sql new file mode 100644 index 000000000..27e472017 --- /dev/null +++ b/crates/pgls_schema_cache/src/queries/indexes.sql @@ -0,0 +1,11 @@ +SELECT + c.oid::bigint as "id!", + n.nspname as "schema!", + c.relname as "name!", + t.relname as "table_name!" +FROM pg_catalog.pg_class c +JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace +JOIN pg_catalog.pg_index i ON i.indexrelid = c.oid +JOIN pg_catalog.pg_class t ON t.oid = i.indrelid +WHERE c.relkind = 'i' + AND n.nspname NOT IN ('pg_catalog', 'information_schema', 'pg_toast') diff --git a/crates/pgls_schema_cache/src/queries/sequences.sql b/crates/pgls_schema_cache/src/queries/sequences.sql new file mode 100644 index 000000000..dc1d64a29 --- /dev/null +++ b/crates/pgls_schema_cache/src/queries/sequences.sql @@ -0,0 +1,8 @@ +SELECT + c.oid::bigint as "id!", + n.nspname as "schema!", + c.relname as "name!" +FROM pg_catalog.pg_class c +JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace +WHERE c.relkind = 'S' + AND n.nspname NOT IN ('pg_catalog', 'information_schema', 'pg_toast') diff --git a/crates/pgls_schema_cache/src/schema_cache.rs b/crates/pgls_schema_cache/src/schema_cache.rs index e56eae3e2..bf8f987c3 100644 --- a/crates/pgls_schema_cache/src/schema_cache.rs +++ b/crates/pgls_schema_cache/src/schema_cache.rs @@ -4,8 +4,10 @@ use sqlx::postgres::PgPool; use crate::columns::Column; use crate::functions::Function; +use crate::indexes::Index; use crate::policies::Policy; use crate::schemas::Schema; +use crate::sequences::Sequence; use crate::tables::Table; use crate::types::PostgresType; use crate::versions::Version; @@ -23,6 +25,8 @@ pub struct SchemaCache { pub extensions: Vec, pub triggers: Vec, pub roles: Vec, + pub indexes: Vec, + pub sequences: Vec, } impl SchemaCache { @@ -39,6 +43,8 @@ impl SchemaCache { triggers, roles, extensions, + indexes, + sequences, ) = futures_util::try_join!( Schema::load(pool), Table::load(pool), @@ -50,6 +56,8 @@ impl SchemaCache { Trigger::load(pool), Role::load(pool), Extension::load(pool), + Index::load(pool), + Sequence::load(pool), )?; let version = versions @@ -68,6 +76,8 @@ impl SchemaCache { triggers, roles, extensions, + indexes, + sequences, }) } @@ -105,6 +115,26 @@ impl SchemaCache { self.types.iter().find(|t| t.id == id) } + pub fn find_table_by_id(&self, id: i64) -> Option<&Table> { + self.tables.iter().find(|t| t.id == id) + } + + pub fn find_function_by_id(&self, id: i64) -> Option<&Function> { + self.functions.iter().find(|f| f.id == id) + } + + pub fn find_schema_by_id(&self, id: i64) -> Option<&Schema> { + self.schemas.iter().find(|s| s.id == id) + } + + pub fn find_index_by_id(&self, id: i64) -> Option<&Index> { + self.indexes.iter().find(|i| i.id == id) + } + + pub fn find_sequence_by_id(&self, id: i64) -> Option<&Sequence> { + self.sequences.iter().find(|s| s.id == id) + } + pub fn find_cols(&self, name: &str, table: Option<&str>, schema: Option<&str>) -> Vec<&Column> { let sanitized_name = Self::sanitize_identifier(name); self.columns diff --git a/crates/pgls_schema_cache/src/sequences.rs b/crates/pgls_schema_cache/src/sequences.rs new file mode 100644 index 000000000..61146d6be --- /dev/null +++ b/crates/pgls_schema_cache/src/sequences.rs @@ -0,0 +1,20 @@ +use sqlx::PgPool; + +use crate::schema_cache::SchemaCacheItem; + +#[derive(Debug, Default, PartialEq, Eq)] +pub struct Sequence { + pub id: i64, + pub schema: String, + pub name: String, +} + +impl SchemaCacheItem for Sequence { + type Item = Sequence; + + async fn load(pool: &PgPool) -> Result, sqlx::Error> { + sqlx::query_file_as!(Sequence, "src/queries/sequences.sql") + .fetch_all(pool) + .await + } +} From e62334ad28b7ff50b1b82507e0a412d52e2bc873 Mon Sep 17 00:00:00 2001 From: psteinroe Date: Mon, 2 Feb 2026 11:48:13 +0100 Subject: [PATCH 11/16] feat(pglinter): integrate rule_messages from pglinter v1.1.0+ - Add RuleMessage struct to fetch messages from pglinter.rule_messages table - Update PglinterCache to store rule_messages HashMap - Update from_violation() to use dynamic messages with {object} placeholder - Fall back to hardcoded messages for older pglinter versions - Add feature gates to indexes.rs and sequences.rs for WASM compatibility --- .gitignore | 1 + Cargo.lock | 1822 +++++++++++---------- crates/pgls_pglinter/src/cache.rs | 75 +- crates/pgls_pglinter/src/diagnostics.rs | 58 +- crates/pgls_pglinter/src/lib.rs | 30 +- crates/pgls_schema_cache/src/indexes.rs | 7 +- crates/pgls_schema_cache/src/sequences.rs | 7 +- 7 files changed, 1130 insertions(+), 870 deletions(-) diff --git a/.gitignore b/.gitignore index af3907006..abab0fa64 100644 --- a/.gitignore +++ b/.gitignore @@ -52,3 +52,4 @@ site/ biome-main/ .review/ pglinter_repo/ +.review/ diff --git a/Cargo.lock b/Cargo.lock index f8091f5f7..659e8c82f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,27 +4,27 @@ version = 4 [[package]] name = "addr2line" -version = "0.24.2" +version = "0.25.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" +checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b" dependencies = [ "gimli", ] [[package]] name = "adler2" -version = "2.0.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" [[package]] name = "ahash" -version = "0.8.11" +version = "0.8.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" dependencies = [ "cfg-if", - "getrandom 0.2.15", + "getrandom 0.3.4", "once_cell", "version_check", "zerocopy", @@ -32,9 +32,9 @@ dependencies = [ [[package]] name = "aho-corasick" -version = "1.1.3" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" dependencies = [ "memchr", ] @@ -53,9 +53,9 @@ checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" [[package]] name = "anstream" -version = "0.6.18" +version = "0.6.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" +checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" dependencies = [ "anstyle", "anstyle-parse", @@ -68,43 +68,44 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.10" +version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" +checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" [[package]] name = "anstyle-parse" -version = "0.2.6" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" -version = "1.1.2" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] name = "anstyle-wincon" -version = "3.0.6" +version = "3.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2109dbce0e72be3ec00bed26e6a7479ca384ad226efdd66db8fa2e3a38c83125" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", - "windows-sys 0.59.0", + "once_cell_polyfill", + "windows-sys 0.60.2", ] [[package]] name = "anyhow" -version = "1.0.94" +version = "1.0.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1fd03a028ef38ba2276dce7e33fcd6369c158a1bca17946c4b1b701891c1ff7" +checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" [[package]] name = "arrayref" @@ -120,13 +121,12 @@ checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b" [[package]] name = "assert_cmd" -version = "2.0.16" +version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc1835b7f27878de8525dc71410b5a31cdcc5f230aed5ba5df968e09c201b23d" +checksum = "9c5bcfa8749ac45dd12cb11055aeeb6b27a3895560d60d71e3c23bf979e60514" dependencies = [ "anstyle", "bstr", - "doc-comment", "libc", "predicates", "predicates-core", @@ -147,9 +147,9 @@ dependencies = [ [[package]] name = "async-channel" -version = "2.3.1" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89b47800b0be77592da0afd425cc03468052844aff33b84e33cc696f64e77b6a" +checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2" dependencies = [ "concurrent-queue", "event-listener-strategy", @@ -159,14 +159,15 @@ dependencies = [ [[package]] name = "async-executor" -version = "1.13.1" +version = "1.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30ca9a001c1e8ba5149f91a74362376cc6bc5b919d92d988668657bd570bdcec" +checksum = "497c00e0fd83a72a79a39fcbd8e3e2f055d6f6c7e025f3b3d91f4f8e76527fb8" dependencies = [ "async-task", "concurrent-queue", "fastrand 2.3.0", - "futures-lite 2.5.0", + "futures-lite 2.6.1", + "pin-project-lite", "slab", ] @@ -176,12 +177,12 @@ version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "05b1b633a2115cd122d73b955eadd9916c18c8f510ec9cd1686404c60ad1c29c" dependencies = [ - "async-channel 2.3.1", + "async-channel 2.5.0", "async-executor", - "async-io 2.4.0", - "async-lock 3.4.0", + "async-io 2.6.0", + "async-lock 3.4.2", "blocking", - "futures-lite 2.5.0", + "futures-lite 2.6.1", "once_cell", ] @@ -207,21 +208,20 @@ dependencies = [ [[package]] name = "async-io" -version = "2.4.0" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43a2b323ccce0a1d90b449fd71f2a06ca7faa7c54c2751f06c9bd851fc061059" +checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc" dependencies = [ - "async-lock 3.4.0", + "autocfg", "cfg-if", "concurrent-queue", "futures-io", - "futures-lite 2.5.0", + "futures-lite 2.6.1", "parking", - "polling 3.7.4", - "rustix 0.38.42", + "polling 3.11.0", + "rustix 1.1.3", "slab", - "tracing", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -235,30 +235,30 @@ dependencies = [ [[package]] name = "async-lock" -version = "3.4.0" +version = "3.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff6e472cdea888a4bd64f342f09b3f50e1886d32afe8df3d663c01140b811b18" +checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311" dependencies = [ - "event-listener 5.3.1", + "event-listener 5.4.1", "event-listener-strategy", "pin-project-lite", ] [[package]] name = "async-std" -version = "1.13.0" +version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c634475f29802fde2b8f0b505b1bd00dfe4df7d4a000f0b36f7671197d5c3615" +checksum = "2c8e079a4ab67ae52b7403632e4618815d6db36d2a010cfe41b02c1b1578f93b" dependencies = [ "async-channel 1.9.0", "async-global-executor", - "async-io 2.4.0", - "async-lock 3.4.0", + "async-io 2.6.0", + "async-lock 3.4.2", "crossbeam-utils", "futures-channel", "futures-core", "futures-io", - "futures-lite 2.5.0", + "futures-lite 2.6.1", "gloo-timers", "kv-log-macro", "log", @@ -278,13 +278,13 @@ checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" [[package]] name = "async-trait" -version = "0.1.83" +version = "0.1.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "721cae7de5c34fbb2acd27e21e6d2cf7b886dce0c27388d46c4e6c47ea4318dd" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.90", + "syn 2.0.114", ] [[package]] @@ -304,26 +304,26 @@ checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" [[package]] name = "auto_impl" -version = "1.2.0" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c87f3f15e7794432337fc718554eaa4dc8f04c9677a950ffe366f20a162ae42" +checksum = "ffdcb70bdbc4d478427380519163274ac86e52916e10f0a8889adf0f96d3fee7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.90", + "syn 2.0.114", ] [[package]] name = "autocfg" -version = "1.4.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "backtrace" -version = "0.3.74" +version = "0.3.76" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" +checksum = "bb531853791a215d7c62a30daf0dde835f381ab5de4589cfe7c649d2cbe92bd6" dependencies = [ "addr2line", "cfg-if", @@ -331,7 +331,7 @@ dependencies = [ "miniz_oxide", "object", "rustc-demangle", - "windows-targets 0.52.6", + "windows-link", ] [[package]] @@ -348,9 +348,9 @@ checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "base64ct" -version = "1.6.0" +version = "1.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" [[package]] name = "beef" @@ -360,22 +360,22 @@ checksum = "3a8241f3ebb85c056b509d4327ad0358fbbba6ffb340bf388f26350aeda225b1" [[package]] name = "bindgen" -version = "0.72.0" +version = "0.72.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f72209734318d0b619a5e0f5129918b848c416e122a3c4ce054e03cb87b726f" +checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.10.0", "cexpr", "clang-sys", - "itertools 0.10.5", + "itertools 0.13.0", "log", "prettyplease", "proc-macro2", "quote", "regex", - "rustc-hash 2.1.0", + "rustc-hash 2.1.1", "shlex", - "syn 2.0.90", + "syn 2.0.114", ] [[package]] @@ -390,7 +390,7 @@ dependencies = [ "serde", "termcolor", "unicode-segmentation", - "unicode-width", + "unicode-width 0.1.14", ] [[package]] @@ -405,7 +405,7 @@ dependencies = [ "biome_json_parser", "biome_json_syntax", "biome_rowan", - "bitflags 2.6.0", + "bitflags 2.10.0", "indexmap 1.9.3", "serde", "serde_json", @@ -424,8 +424,8 @@ dependencies = [ "biome_json_parser", "biome_json_syntax", "biome_rowan", - "bitflags 2.6.0", - "indexmap 2.7.0", + "bitflags 2.10.0", + "indexmap 2.13.0", "schemars", "serde", ] @@ -469,12 +469,12 @@ dependencies = [ "biome_rowan", "biome_text_edit", "biome_text_size", - "bitflags 2.6.0", + "bitflags 2.10.0", "bpaf", "oxc_resolver", "serde", "termcolor", - "unicode-width", + "unicode-width 0.1.14", ] [[package]] @@ -516,7 +516,7 @@ dependencies = [ "indexmap 1.9.3", "rustc-hash 1.1.0", "tracing", - "unicode-width", + "unicode-width 0.1.14", ] [[package]] @@ -549,7 +549,7 @@ dependencies = [ "cfg-if", "smallvec", "tracing", - "unicode-width", + "unicode-width 0.1.14", ] [[package]] @@ -621,7 +621,7 @@ dependencies = [ "biome_console", "biome_diagnostics", "biome_rowan", - "bitflags 2.6.0", + "bitflags 2.10.0", "drop_bomb", ] @@ -680,11 +680,11 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.6.0" +version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" +checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" dependencies = [ - "serde", + "serde_core", ] [[package]] @@ -709,22 +709,22 @@ dependencies = [ [[package]] name = "blocking" -version = "1.6.1" +version = "1.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "703f41c54fc768e63e091340b424302bb1c29ef4aa0c7f10fe849dfb114d29ea" +checksum = "e83f8d02be6967315521be875afa792a316e28d57b5a2d401897e2a7921b7f21" dependencies = [ - "async-channel 2.3.1", + "async-channel 2.5.0", "async-task", "futures-io", - "futures-lite 2.5.0", + "futures-lite 2.6.1", "piper", ] [[package]] name = "bpaf" -version = "0.9.15" +version = "0.9.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50fd5174866dc2fa2ddc96e8fb800852d37f064f32a45c7b7c2f8fa2c64c77fa" +checksum = "4ffe12d9bbc54238745f1749c5a18dc5759e03c235618dd05554a9be350390b1" dependencies = [ "bpaf_derive", "owo-colors", @@ -733,31 +733,31 @@ dependencies = [ [[package]] name = "bpaf_derive" -version = "0.5.13" +version = "0.5.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf95d9c7e6aba67f8fc07761091e93254677f4db9e27197adecebc7039a58722" +checksum = "9e7d8cbf45f5994072fad3eb22761aacbe8752bbe733a247f7d35827556ea057" dependencies = [ "proc-macro2", "quote", - "syn 2.0.90", + "syn 2.0.114", ] [[package]] name = "bstr" -version = "1.11.0" +version = "1.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a68f1f47cdf0ec8ee4b941b2eee2a80cb796db73118c0dd09ac63fbe405be22" +checksum = "63044e1ae8e69f3b5a92c736ca6269b8d12fa7efe39bf34ddb06d102cf0e2cab" dependencies = [ "memchr", - "regex-automata 0.4.13", + "regex-automata", "serde", ] [[package]] name = "bumpalo" -version = "3.16.0" +version = "3.19.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" +checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" [[package]] name = "byteorder" @@ -767,9 +767,15 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.9.0" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" + +[[package]] +name = "camino" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "325918d6fe32f23b19878fe4b34794ae41fc19ddbe53b10571a4874d44ffd39b" +checksum = "e629a66d692cb9ff1a1c664e41771b3dcaf961985a9774c0eb0bd1b51cf60a48" [[package]] name = "cast" @@ -779,10 +785,11 @@ checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" [[package]] name = "cc" -version = "1.2.27" +version = "1.2.54" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d487aa071b5f64da6f19a3e848e3578944b726ee5a4854b82172f02aa876bfdc" +checksum = "6354c81bbfd62d9cfa9cb3c773c2b7b2a3a482d569de977fd0e961f6e7c00583" dependencies = [ + "find-msvc-tools", "shlex", ] @@ -797,15 +804,15 @@ dependencies = [ [[package]] name = "cfg-if" -version = "1.0.0" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "chrono" -version = "0.4.39" +version = "0.4.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e36cc9d416881d2e24f9a963be5fb1cd90966419ac844274161d10488b3e825" +checksum = "fac4744fb15ae8337dc853fee7fb3f4e48c0fbaa23d0afe49c447b4fab126118" dependencies = [ "num-traits", ] @@ -850,9 +857,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.23" +version = "4.5.54" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3135e7ec2ef7b10c6ed8950f0f792ed96ee093fa088608f1c76e569722700c84" +checksum = "c6e6ff9dcd79cff5cd969a17a545d79e84ab086e444102a591e288a8aa3ce394" dependencies = [ "clap_builder", "clap_derive", @@ -860,9 +867,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.23" +version = "4.5.54" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30582fc632330df2bd26877bde0c1f4470d57c582bbc070376afcd04d8cb4838" +checksum = "fa42cf4d2b7a41bc8f663a7cab4031ebafa1bf3875705bfaf8466dc60ab52c00" dependencies = [ "anstream", "anstyle", @@ -872,21 +879,21 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.18" +version = "4.5.49" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ac6a0c7b1a9e9a5186361f67dfa1b88213572f427fb9ab038efb2bd8c582dab" +checksum = "2a0b5487afeab2deb2ff4e03a807ad1a03ac532ff5a2cee5d86884440c7f7671" dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.90", + "syn 2.0.114", ] [[package]] name = "clap_lex" -version = "0.7.4" +version = "0.7.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" +checksum = "c3e64b0cc0439b12df2fa678eae89a1c56a529fd067a9115f7827f1fffd22b32" [[package]] name = "clippy" @@ -899,9 +906,9 @@ dependencies = [ [[package]] name = "colorchoice" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" [[package]] name = "concurrent-queue" @@ -914,14 +921,14 @@ dependencies = [ [[package]] name = "console" -version = "0.15.8" +version = "0.15.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e1f83fc076bd6dd27517eacdf25fef6c4dfe5f1d7448bafaaf3a26f13b5e4eb" +checksum = "054ccb5b10f9f2cbf51eb355ca1d05c2d279ce1804688d0db74b4733a5aeafd8" dependencies = [ "encode_unicode", - "lazy_static", "libc", - "windows-sys 0.52.0", + "once_cell", + "windows-sys 0.59.0", ] [[package]] @@ -953,18 +960,18 @@ checksum = "7704b5fdd17b18ae31c4c1da5a2e0305a2bf17b5249300a9ee9ed7b72114c636" [[package]] name = "cpufeatures" -version = "0.2.16" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16b80225097f2e5ae4e7179dd2266824648f3e2f49d9134d584b76389d31c4c3" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" dependencies = [ "libc", ] [[package]] name = "crc" -version = "3.2.1" +version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69e6e4d7b33a94f0991c26729976b10ebde1d34c3ee82408fb536164fa10d636" +checksum = "5eb8a2a1cd12ab0d987a5d5e825195d372001a4094a0376319d5a0ad71c1ba0d" dependencies = [ "crc-catalog", ] @@ -977,9 +984,9 @@ checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" [[package]] name = "crc32fast" -version = "1.4.2" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" dependencies = [ "cfg-if", ] @@ -1035,18 +1042,18 @@ dependencies = [ [[package]] name = "crossbeam-channel" -version = "0.5.13" +version = "0.5.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33480d6946193aa8033910124896ca395333cae7e2d1113d1fef6c3272217df2" +checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" dependencies = [ "crossbeam-utils", ] [[package]] name = "crossbeam-deque" -version = "0.8.5" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" dependencies = [ "crossbeam-epoch", "crossbeam-utils", @@ -1063,30 +1070,30 @@ dependencies = [ [[package]] name = "crossbeam-queue" -version = "0.3.11" +version = "0.3.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df0346b5d5e76ac2fe4e327c5fd1118d6be7c51dfb18f9b7922923f287471e35" +checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115" dependencies = [ "crossbeam-utils", ] [[package]] name = "crossbeam-utils" -version = "0.8.20" +version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" [[package]] name = "crunchy" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43da5946c66ffcc7745f48db692ffbb10a83bfe0afd96235c5c2a4fb23994929" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" [[package]] name = "crypto-common" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" dependencies = [ "generic-array", "typenum", @@ -1121,9 +1128,9 @@ dependencies = [ [[package]] name = "der" -version = "0.7.9" +version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f55bf8e7b65898637379c1b74eb1551107c8294ed26d855ceb9fd1a09cfc9bc0" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" dependencies = [ "const-oid", "pem-rfc7468", @@ -1132,9 +1139,9 @@ dependencies = [ [[package]] name = "deranged" -version = "0.3.11" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" +checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587" dependencies = [ "powerfmt", ] @@ -1157,6 +1164,27 @@ dependencies = [ "subtle", ] +[[package]] +name = "dir-test" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62c013fe825864f3e4593f36426c1fa7a74f5603f13ca8d1af7a990c1cd94a79" +dependencies = [ + "dir-test-macros", +] + +[[package]] +name = "dir-test-macros" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d42f54d7b4a6bc2400fe5b338e35d1a335787585375322f49c5d5fe7b243da7e" +dependencies = [ + "glob", + "proc-macro2", + "quote", + "syn 2.0.114", +] + [[package]] name = "directories" version = "5.0.1" @@ -1197,15 +1225,9 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.90", + "syn 2.0.114", ] -[[package]] -name = "doc-comment" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" - [[package]] name = "docs_codegen" version = "0.0.0" @@ -1258,9 +1280,9 @@ checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" [[package]] name = "dyn-clone" -version = "1.0.17" +version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d6ef0072f8a535281e4876be788938b528e9a1d43900b82c2569af7da799125" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" [[package]] name = "easy-parallel" @@ -1270,53 +1292,53 @@ checksum = "2afbb9b0aef60e4f0d2b18129b6c0dff035a6f7dbbd17c2f38c1432102ee223c" [[package]] name = "either" -version = "1.13.0" +version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" dependencies = [ "serde", ] [[package]] name = "encode_unicode" -version = "0.3.6" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" +checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" [[package]] name = "enumflags2" -version = "0.7.11" +version = "0.7.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba2f4b465f5318854c6f8dd686ede6c0a9dc67d4b1ac241cf0eb51521a309147" +checksum = "1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef" dependencies = [ "enumflags2_derive", ] [[package]] name = "enumflags2_derive" -version = "0.7.11" +version = "0.7.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc4caf64a58d7a6d65ab00639b046ff54399a39f5f2554728895ace4b297cd79" +checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" dependencies = [ "proc-macro2", "quote", - "syn 2.0.90", + "syn 2.0.114", ] [[package]] name = "env_filter" -version = "0.1.3" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "186e05a59d4c50738528153b83b0b0194d3a29507dfec16eccd4b342903397d0" +checksum = "1bf3c259d255ca70051b30e2e95b5446cdb8949ac4cd22c0d7fd634d89f568e2" dependencies = [ "log", ] [[package]] name = "env_logger" -version = "0.11.7" +version = "0.11.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3716d7a920fb4fac5d84e9d4bce8ceb321e9414b4409da61b07b75c1e3d0697" +checksum = "13c863f0904021b108aa8b2f55046443e6b1ebde8fd4a15c399893aae4fa069f" dependencies = [ "anstream", "anstyle", @@ -1326,15 +1348,15 @@ dependencies = [ [[package]] name = "equivalent" -version = "1.0.1" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "errno" -version = "0.3.10" +version = "0.3.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", "windows-sys 0.59.0", @@ -1359,9 +1381,9 @@ checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" [[package]] name = "event-listener" -version = "5.3.1" +version = "5.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6032be9bd27023a771701cc49f9f053c751055f71efb2e0ae5c15809093675ba" +checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" dependencies = [ "concurrent-queue", "parking", @@ -1370,11 +1392,11 @@ dependencies = [ [[package]] name = "event-listener-strategy" -version = "0.5.3" +version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c3e4e0dd3673c1139bf041f3008816d9cf2946bbfac2945c09e523b8d7b05b2" +checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" dependencies = [ - "event-listener 5.3.1", + "event-listener 5.4.1", "pin-project-lite", ] @@ -1393,22 +1415,37 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +[[package]] +name = "find-msvc-tools" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8591b0bcc8a98a64310a2fae1bb3e9b8564dd10e381e6e28010fde8e8e8568db" + [[package]] name = "fixedbitset" -version = "0.4.2" +version = "0.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" +checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" [[package]] name = "flate2" -version = "1.0.35" +version = "1.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c936bfdafb507ebbf50b8074c54fa31c5be9a1e7e5f467dd659697041407d07c" +checksum = "b375d6465b98090a5f25b1c7703f3859783755aa9a80433b36e0379a3ec2f369" dependencies = [ "crc32fast", "miniz_oxide", ] +[[package]] +name = "fluent-uri" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17c704e9dbe1ddd863da1e6ff3567795087b1eb201ce80d8fa81162e1516500d" +dependencies = [ + "bitflags 1.3.2", +] + [[package]] name = "flume" version = "0.11.1" @@ -1434,9 +1471,9 @@ checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" [[package]] name = "form_urlencoded" -version = "1.2.1" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" dependencies = [ "percent-encoding", ] @@ -1523,9 +1560,9 @@ dependencies = [ [[package]] name = "futures-lite" -version = "2.5.0" +version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cef40d21ae2c515b51041df9ed313ed21e572df340ea58a922a0aefe7e8891a1" +checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" dependencies = [ "fastrand 2.3.0", "futures-core", @@ -1542,7 +1579,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn 2.0.90", + "syn 2.0.114", ] [[package]] @@ -1606,11 +1643,11 @@ dependencies = [ [[package]] name = "getopts" -version = "0.2.21" +version = "0.2.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14dbbfd5c71d70241ecf9e6f13737f7b5ce823821063188d7e46c41d371eebd5" +checksum = "cfe4fbac503b8d1f88e6676011885f34b7174f46e59956bba534ba83abded4df" dependencies = [ - "unicode-width", + "unicode-width 0.2.2", ] [[package]] @@ -1626,50 +1663,50 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.15" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" dependencies = [ "cfg-if", "libc", - "wasi 0.11.0+wasi-snapshot-preview1", + "wasi 0.11.1+wasi-snapshot-preview1", ] [[package]] name = "getrandom" -version = "0.3.3" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ "cfg-if", "libc", "r-efi", - "wasi 0.14.7+wasi-0.2.4", + "wasip2", ] [[package]] name = "gimli" -version = "0.31.1" +version = "0.32.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" +checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" [[package]] name = "glob" -version = "0.3.1" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" [[package]] name = "globset" -version = "0.4.16" +version = "0.4.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54a1028dfc5f5df5da8a56a73e6c153c9a9708ec57232470703592a3f18e49f5" +checksum = "52dfc19153a48bde0cbd630453615c8151bce3a5adfac7a0aebfbf0a1e1f57e3" dependencies = [ "aho-corasick", "bstr", "log", - "regex-automata 0.4.13", - "regex-syntax 0.8.5", + "regex-automata", + "regex-syntax", ] [[package]] @@ -1678,7 +1715,7 @@ version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0bf760ebf69878d9fd8f110c89703d90ce35095324d1f1edcb595c63945ee757" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.10.0", "ignore", "walkdir", ] @@ -1697,12 +1734,13 @@ dependencies = [ [[package]] name = "half" -version = "2.6.0" +version = "2.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "459196ed295495a68f7d7fe1d84f6c4b7ff0e21fe3017b2f283c6fac3ad803c9" +checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" dependencies = [ "cfg-if", "crunchy", + "zerocopy", ] [[package]] @@ -1716,29 +1754,31 @@ name = "hashbrown" version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" -dependencies = [ - "ahash", - "allocator-api2", -] [[package]] name = "hashbrown" -version = "0.15.2" +version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" dependencies = [ "allocator-api2", "equivalent", "foldhash", ] +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" + [[package]] name = "hashlink" -version = "0.9.1" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ba4ff7128dee98c7dc9794b6a411377e1404dba1c97deb8d1a55297bd25d8af" +checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" dependencies = [ - "hashbrown 0.14.5", + "hashbrown 0.15.5", ] [[package]] @@ -1765,15 +1805,9 @@ checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" [[package]] name = "hermit-abi" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbf6a919d6cf397374f7dfeeea91d974c7c0a7221d0d0f4f20d859d329e53fcc" - -[[package]] -name = "hermit-abi" -version = "0.5.0" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbd780fe5cc30f81464441920d82ac8740e2e46b29a6fad543ddd075229ce37e" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" [[package]] name = "hex" @@ -1801,18 +1835,18 @@ dependencies = [ [[package]] name = "home" -version = "0.5.9" +version = "0.5.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5" +checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] name = "httparse" -version = "1.9.5" +version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d71d3574edd2771538b901e6549113b4006ece66150fb69c0fb6d9a2adae946" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" [[package]] name = "humansize" @@ -1825,21 +1859,22 @@ dependencies = [ [[package]] name = "icu_collections" -version = "1.5.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" +checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" dependencies = [ "displaydoc", + "potential_utf", "yoke", "zerofrom", "zerovec", ] [[package]] -name = "icu_locid" -version = "1.5.0" +name = "icu_locale_core" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" +checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" dependencies = [ "displaydoc", "litemap", @@ -1848,104 +1883,66 @@ dependencies = [ "zerovec", ] -[[package]] -name = "icu_locid_transform" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" -dependencies = [ - "displaydoc", - "icu_locid", - "icu_locid_transform_data", - "icu_provider", - "tinystr", - "zerovec", -] - -[[package]] -name = "icu_locid_transform_data" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e" - [[package]] name = "icu_normalizer" -version = "1.5.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" +checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" dependencies = [ - "displaydoc", "icu_collections", "icu_normalizer_data", "icu_properties", "icu_provider", "smallvec", - "utf16_iter", - "utf8_iter", - "write16", "zerovec", ] [[package]] name = "icu_normalizer_data" -version = "1.5.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516" +checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" [[package]] name = "icu_properties" -version = "1.5.1" +version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" +checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" dependencies = [ - "displaydoc", "icu_collections", - "icu_locid_transform", + "icu_locale_core", "icu_properties_data", "icu_provider", - "tinystr", + "zerotrie", "zerovec", ] [[package]] name = "icu_properties_data" -version = "1.5.0" +version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569" +checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" [[package]] name = "icu_provider" -version = "1.5.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" +checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" dependencies = [ "displaydoc", - "icu_locid", - "icu_provider_macros", - "stable_deref_trait", - "tinystr", + "icu_locale_core", "writeable", "yoke", "zerofrom", + "zerotrie", "zerovec", ] -[[package]] -name = "icu_provider_macros" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.90", -] - [[package]] name = "idna" -version = "1.0.3" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" dependencies = [ "idna_adapter", "smallvec", @@ -1954,9 +1951,9 @@ dependencies = [ [[package]] name = "idna_adapter" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" dependencies = [ "icu_normalizer", "icu_properties", @@ -1964,15 +1961,15 @@ dependencies = [ [[package]] name = "ignore" -version = "0.4.23" +version = "0.4.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d89fd380afde86567dfba715db065673989d6253f42b88179abd3eae47bda4b" +checksum = "d3d782a365a015e0f5c04902246139249abf769125006fbe7649e2ee88169b4a" dependencies = [ "crossbeam-deque", "globset", "log", "memchr", - "regex-automata 0.4.13", + "regex-automata", "same-file", "walkdir", "winapi-util", @@ -1991,27 +1988,27 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.7.0" +version = "2.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62f822373a4fe84d4bb149bf54e584a7f4abec90e072ed49cda0edea5b95471f" +checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" dependencies = [ "equivalent", - "hashbrown 0.15.2", + "hashbrown 0.16.1", "serde", + "serde_core", ] [[package]] name = "insta" -version = "1.42.1" +version = "1.46.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71c1b125e30d93896b365e156c33dadfffab45ee8400afcbba4752f59de08a86" +checksum = "248b42847813a1550dafd15296fd9748c651d0c32194559dbc05d804d54b21e8" dependencies = [ "console", - "linked-hash-map", "once_cell", - "pin-project", "serde", "similar", + "tempfile", ] [[package]] @@ -2036,11 +2033,11 @@ dependencies = [ [[package]] name = "is-terminal" -version = "0.4.16" +version = "0.4.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e04d7f318608d35d4b61ddd75cbdaee86b023ebe2bd5a66ee0915f0bf93095a9" +checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46" dependencies = [ - "hermit-abi 0.5.0", + "hermit-abi 0.5.2", "libc", "windows-sys 0.59.0", ] @@ -2053,9 +2050,9 @@ checksum = "7655c9839580ee829dfacba1d1278c2b7883e50a277ff7541299489d6bdfdc45" [[package]] name = "is_terminal_polyfill" -version = "1.70.1" +version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" [[package]] name = "itertools" @@ -2066,6 +2063,15 @@ dependencies = [ "either", ] +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + [[package]] name = "itertools" version = "0.14.0" @@ -2077,15 +2083,15 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.14" +version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" [[package]] name = "js-sys" -version = "0.3.81" +version = "0.3.85" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec48937a97411dcb524a265206ccd4c90bb711fca92b2792c407f268825b9305" +checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3" dependencies = [ "once_cell", "wasm-bindgen", @@ -2120,31 +2126,31 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.168" +version = "0.2.180" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5aaeb2981e0606ca11d79718f8bb01164f1d6ed75080182d3abf017e6d244b6d" +checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc" [[package]] name = "libloading" -version = "0.8.6" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" dependencies = [ "cfg-if", - "windows-targets 0.52.6", + "windows-link", ] [[package]] name = "libm" -version = "0.2.11" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8355be11b20d696c8f18f6cc018c4e372165b1fa8126cef092399c9951984ffa" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" [[package]] name = "libmimalloc-sys" -version = "0.1.39" +version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23aa6811d3bd4deb8a84dde645f943476d13b248d818edcf8ce0b2f37f036b44" +checksum = "667f4fec20f29dfc6bc7357c582d91796c169ad7e2fce709468aefeb2c099870" dependencies = [ "cc", "libc", @@ -2152,12 +2158,13 @@ dependencies = [ [[package]] name = "libredox" -version = "0.1.3" +version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" +checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.10.0", "libc", + "redox_syscall 0.7.0", ] [[package]] @@ -2166,84 +2173,82 @@ version = "0.30.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149" dependencies = [ - "cc", "pkg-config", "vcpkg", ] [[package]] -name = "linked-hash-map" -version = "0.5.6" +name = "linux-raw-sys" +version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" +checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" [[package]] name = "linux-raw-sys" -version = "0.3.8" +version = "0.4.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" [[package]] name = "linux-raw-sys" -version = "0.4.14" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" +checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" [[package]] name = "litemap" -version = "0.7.4" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ee93343901ab17bd981295f2cf0026d4ad018c7c31ba84549a4ddbb47a45104" +checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" [[package]] name = "lock_api" -version = "0.4.12" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" dependencies = [ - "autocfg", "scopeguard", ] [[package]] name = "log" -version = "0.4.22" +version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" dependencies = [ "value-bag", ] [[package]] name = "logos" -version = "0.15.0" +version = "0.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab6f536c1af4c7cc81edf73da1f8029896e7e1e16a219ef09b184e76a296f3db" +checksum = "ff472f899b4ec2d99161c51f60ff7075eeb3097069a36050d8037a6325eb8154" dependencies = [ "logos-derive", ] [[package]] name = "logos-codegen" -version = "0.15.0" +version = "0.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "189bbfd0b61330abea797e5e9276408f2edbe4f822d7ad08685d67419aafb34e" +checksum = "192a3a2b90b0c05b27a0b2c43eecdb7c415e29243acc3f89cc8247a5b693045c" dependencies = [ "beef", "fnv", "lazy_static", "proc-macro2", "quote", - "regex-syntax 0.8.5", + "regex-syntax", "rustc_version", - "syn 2.0.90", + "syn 2.0.114", ] [[package]] name = "logos-derive" -version = "0.15.0" +version = "0.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebfe8e1a19049ddbfccbd14ac834b215e11b85b90bab0c2dba7c7b92fb5d5cba" +checksum = "605d9697bcd5ef3a42d38efc51541aa3d6a4a25f7ab6d1ed0da5ac632a26b470" dependencies = [ "logos-codegen", ] @@ -2254,7 +2259,7 @@ version = "0.12.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38" dependencies = [ - "hashbrown 0.15.2", + "hashbrown 0.15.5", ] [[package]] @@ -2270,13 +2275,26 @@ dependencies = [ "url", ] +[[package]] +name = "lsp-types" +version = "0.97.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53353550a17c04ac46c585feb189c2db82154fc84b79c7a66c96c2c644f66071" +dependencies = [ + "bitflags 1.3.2", + "fluent-uri", + "serde", + "serde_json", + "serde_repr", +] + [[package]] name = "matchers" -version = "0.1.0" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" dependencies = [ - "regex-automata 0.1.10", + "regex-automata", ] [[package]] @@ -2291,9 +2309,9 @@ dependencies = [ [[package]] name = "memchr" -version = "2.7.4" +version = "2.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" [[package]] name = "memoffset" @@ -2312,7 +2330,7 @@ checksum = "5f98efec8807c63c752b5bd61f862c165c115b0a35685bdcfd9238c7aeb592b7" dependencies = [ "cfg-if", "miette-derive", - "unicode-width", + "unicode-width 0.1.14", ] [[package]] @@ -2323,14 +2341,14 @@ checksum = "db5b29714e950dbb20d5e6f74f9dcec4edbcc1067bb7f8ed198c097b8c1a818b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.90", + "syn 2.0.114", ] [[package]] name = "mimalloc" -version = "0.1.43" +version = "0.1.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68914350ae34959d83f732418d51e2427a794055d0b9529f48259ac07af65633" +checksum = "e1ee66a4b64c74f4ef288bcbb9192ad9c3feaad75193129ac8509af543894fd8" dependencies = [ "libmimalloc-sys", ] @@ -2343,35 +2361,36 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "miniz_oxide" -version = "0.8.0" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" dependencies = [ "adler2", + "simd-adler32", ] [[package]] name = "mio" -version = "1.0.3" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" +checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" dependencies = [ "libc", - "wasi 0.11.0+wasi-snapshot-preview1", - "windows-sys 0.52.0", + "wasi 0.11.1+wasi-snapshot-preview1", + "windows-sys 0.61.2", ] [[package]] name = "multimap" -version = "0.8.3" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5ce46fe64a9d73be07dcbe690a38ce1b293be448fd8ce1e6c1b8062c9f72c6a" +checksum = "1d87ecb2933e8aeadb3e3a02b828fed80a7528047e68b4f424523a0981a3a084" [[package]] name = "newtype-uuid" -version = "1.1.3" +version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c8781e2ef64806278a55ad223f0bc875772fd40e1fe6e73e8adbf027817229d" +checksum = "5c012d14ef788ab066a347d19e3dda699916c92293b05b85ba2c76b8c82d2830" dependencies = [ "uuid", ] @@ -2388,9 +2407,9 @@ dependencies = [ [[package]] name = "ntest" -version = "0.9.3" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb183f0a1da7a937f672e5ee7b7edb727bf52b8a52d531374ba8ebb9345c0330" +checksum = "54d1aa56874c2152c24681ed0df95ee155cc06c5c61b78e2d1e8c0cae8bc5326" dependencies = [ "ntest_test_cases", "ntest_timeout", @@ -2398,9 +2417,9 @@ dependencies = [ [[package]] name = "ntest_test_cases" -version = "0.9.3" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16d0d3f2a488592e5368ebbe996e7f1d44aa13156efad201f5b4d84e150eaa93" +checksum = "6913433c6319ef9b2df316bb8e3db864a41724c2bb8f12555e07dc4ec69d3db1" dependencies = [ "proc-macro2", "quote", @@ -2409,9 +2428,9 @@ dependencies = [ [[package]] name = "ntest_timeout" -version = "0.9.3" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcc7c92f190c97f79b4a332f5e81dcf68c8420af2045c936c9be0bc9de6f63b5" +checksum = "9224be3459a0c1d6e9b0f42ab0e76e98b29aef5aba33c0487dfcf47ea08b5150" dependencies = [ "proc-macro-crate", "proc-macro2", @@ -2421,30 +2440,19 @@ dependencies = [ [[package]] name = "nu-ansi-term" -version = "0.46.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" -dependencies = [ - "overload", - "winapi", -] - -[[package]] -name = "nu-ansi-term" -version = "0.50.1" +version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4a28e057d01f97e61255210fcff094d74ed0466038633e95017f5beb68e4399" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] name = "num-bigint-dig" -version = "0.8.4" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc84195820f291c7697304f3cbdadd1cb7199c0efc917ff5eafd71225c136151" +checksum = "e661dda6640fad38e827a6d4a310ff4763082116fe217f279885c97f511bb0b7" dependencies = [ - "byteorder", "lazy_static", "libm", "num-integer", @@ -2457,9 +2465,9 @@ dependencies = [ [[package]] name = "num-conv" -version = "0.1.0" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" +checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050" [[package]] name = "num-integer" @@ -2502,18 +2510,24 @@ dependencies = [ [[package]] name = "object" -version = "0.36.5" +version = "0.37.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aedf0a2d09c573ed1d8d85b30c119153926a2b36dce0ab28322c09a117a4683e" +checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" dependencies = [ "memchr", ] [[package]] name = "once_cell" -version = "1.20.2" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" [[package]] name = "oorandom" @@ -2527,17 +2541,11 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" -[[package]] -name = "overload" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" - [[package]] name = "owo-colors" -version = "4.1.0" +version = "4.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb37767f6569cd834a413442455e0f066d0d522de8630436e2a1761d9726ba56" +checksum = "9c6901729fa79e91a0913333229e9ca5dc725089d1c363b2f4b4760709dc4a52" [[package]] name = "oxc_resolver" @@ -2548,10 +2556,10 @@ dependencies = [ "cfg-if", "dashmap 6.1.0", "dunce", - "indexmap 2.7.0", + "indexmap 2.13.0", "json-strip-comments", "once_cell", - "rustc-hash 2.1.0", + "rustc-hash 2.1.1", "serde", "serde_json", "simdutf8", @@ -2567,9 +2575,9 @@ checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" [[package]] name = "parking_lot" -version = "0.12.3" +version = "0.12.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" dependencies = [ "lock_api", "parking_lot_core", @@ -2577,23 +2585,17 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.10" +version = "0.9.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" dependencies = [ "cfg-if", "libc", - "redox_syscall 0.5.7", + "redox_syscall 0.5.18", "smallvec", - "windows-targets 0.52.6", + "windows-link", ] -[[package]] -name = "paste" -version = "1.0.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" - [[package]] name = "path-absolutize" version = "3.1.1" @@ -2623,18 +2625,18 @@ dependencies = [ [[package]] name = "percent-encoding" -version = "2.3.1" +version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "petgraph" -version = "0.6.5" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db" +checksum = "3672b37090dbd86368a4145bc067582552b29c27377cad4e0a306c97f9bd7772" dependencies = [ "fixedbitset", - "indexmap 2.7.0", + "indexmap 2.13.0", ] [[package]] @@ -2649,7 +2651,7 @@ dependencies = [ "pgls_query", "pgls_schema_cache", "pgls_text_size", - "rustc-hash 2.1.0", + "rustc-hash 2.1.1", "schemars", "serde", ] @@ -2668,7 +2670,7 @@ dependencies = [ "pgls_statement_splitter", "pgls_test_macros", "pgls_text_size", - "rustc-hash 2.1.0", + "rustc-hash 2.1.1", "serde", "termcolor", ] @@ -2696,12 +2698,13 @@ dependencies = [ "pgls_env", "pgls_fs", "pgls_lsp", + "pgls_schema_cache", "pgls_test_utils", "pgls_text_edit", "pgls_workspace", "quick-junit", "rayon", - "rustc-hash 2.1.0", + "rustc-hash 2.1.1", "serde", "serde_json", "sqlx", @@ -2718,7 +2721,6 @@ dependencies = [ name = "pgls_completions" version = "0.0.0" dependencies = [ - "async-std", "criterion", "fuzzy-matcher", "insta", @@ -2732,7 +2734,6 @@ dependencies = [ "serde", "serde_json", "sqlx", - "tokio", "tracing", "tree-sitter", "unindent", @@ -2745,7 +2746,7 @@ dependencies = [ "biome_deserialize 0.6.0", "biome_deserialize_macros 0.6.0", "bpaf", - "indexmap 2.7.0", + "indexmap 2.13.0", "oxc_resolver", "pgls_analyse", "pgls_analyser", @@ -2753,8 +2754,9 @@ dependencies = [ "pgls_diagnostics", "pgls_env", "pgls_matcher", + "pgls_pretty_print", "pgls_text_size", - "rustc-hash 2.1.0", + "rustc-hash 2.1.1", "schemars", "serde", "serde_json", @@ -2771,7 +2773,7 @@ dependencies = [ "termcolor", "trybuild", "unicode-segmentation", - "unicode-width", + "unicode-width 0.1.14", ] [[package]] @@ -2791,7 +2793,7 @@ dependencies = [ "serde", "serde_json", "termcolor", - "unicode-width", + "unicode-width 0.1.14", ] [[package]] @@ -2832,7 +2834,7 @@ dependencies = [ "parking_lot", "pgls_diagnostics", "rayon", - "rustc-hash 2.1.0", + "rustc-hash 2.1.1", "schemars", "serde", "smallvec", @@ -2855,7 +2857,6 @@ dependencies = [ "serde", "serde_json", "sqlx", - "tokio", "tracing", "tree-sitter", ] @@ -2901,7 +2902,7 @@ dependencies = [ "pgls_text_edit", "pgls_text_size", "pgls_workspace", - "rustc-hash 2.1.0", + "rustc-hash 2.1.1", "serde", "serde_json", "sqlx", @@ -2928,7 +2929,7 @@ version = "0.0.0" dependencies = [ "pgls_console", "pgls_diagnostics", - "rustc-hash 2.1.0", + "rustc-hash 2.1.1", ] [[package]] @@ -2942,7 +2943,7 @@ dependencies = [ "pgls_diagnostics_categories", "pgls_schema_cache", "pgls_test_utils", - "rustc-hash 2.1.0", + "rustc-hash 2.1.1", "serde", "serde_json", "sqlx", @@ -2967,30 +2968,57 @@ dependencies = [ ] [[package]] -name = "pgls_query" +name = "pgls_pretty_print" version = "0.0.0" dependencies = [ - "bindgen", - "cc", - "clippy", - "easy-parallel", - "fs_extra", - "glob", - "pgls_query_macros", - "prost", - "prost-build", + "camino", + "dir-test", + "insta", + "pgls_pretty_print_codegen", + "pgls_query", + "pgls_statement_splitter", + "regex", "thiserror 1.0.69", - "which", ] [[package]] -name = "pgls_query_ext" +name = "pgls_pretty_print_codegen" version = "0.0.0" dependencies = [ - "pgls_diagnostics", - "pgls_query", - "pgls_text_size", -] + "anyhow", + "convert_case", + "proc-macro2", + "prost-reflect", + "protox", + "quote", + "ureq", +] + +[[package]] +name = "pgls_query" +version = "0.0.0" +dependencies = [ + "bindgen", + "cc", + "clippy", + "easy-parallel", + "fs_extra", + "glob", + "pgls_query_macros", + "prost", + "prost-build", + "thiserror 1.0.69", + "which", +] + +[[package]] +name = "pgls_query_ext" +version = "0.0.0" +dependencies = [ + "pgls_diagnostics", + "pgls_query", + "pgls_text_size", +] [[package]] name = "pgls_query_macros" @@ -3025,6 +3053,7 @@ dependencies = [ name = "pgls_splinter" version = "0.0.0" dependencies = [ + "biome_deserialize 0.6.0", "insta", "pgls_analyse", "pgls_configuration", @@ -3129,6 +3158,7 @@ name = "pgls_treesitter_grammar" version = "0.0.0" dependencies = [ "cc", + "criterion", "insta", "pgls_test_utils", "tree-sitter", @@ -3167,6 +3197,26 @@ dependencies = [ "uuid", ] +[[package]] +name = "pgls_wasm" +version = "0.0.0" +dependencies = [ + "lsp-types 0.97.0", + "pgls_analyse", + "pgls_completions", + "pgls_configuration", + "pgls_diagnostics", + "pgls_fs", + "pgls_query", + "pgls_schema_cache", + "pgls_text_size", + "pgls_treesitter_grammar", + "pgls_workspace", + "serde", + "serde_json", + "tree-sitter", +] + [[package]] name = "pgls_workspace" version = "0.0.0" @@ -3191,6 +3241,7 @@ dependencies = [ "pgls_lexer", "pgls_matcher", "pgls_plpgsql_check", + "pgls_pretty_print", "pgls_query", "pgls_query_ext", "pgls_schema_cache", @@ -3198,13 +3249,14 @@ dependencies = [ "pgls_statement_splitter", "pgls_suppressions", "pgls_test_utils", + "pgls_text_edit", "pgls_text_size", "pgls_tokenizer", "pgls_treesitter_grammar", "pgls_typecheck", "pgls_workspace_macros", "regex", - "rustc-hash 2.1.0", + "rustc-hash 2.1.1", "schemars", "serde", "serde_json", @@ -3228,29 +3280,29 @@ dependencies = [ [[package]] name = "pin-project" -version = "1.1.7" +version = "1.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be57f64e946e500c8ee36ef6331845d40a93055567ec57e8fae13efd33759b95" +checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "1.1.7" +version = "1.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c0f5fad0874fc7abcd4d750e76917eaebbecaa2c20bde22e1dbeeba8beb758c" +checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" dependencies = [ "proc-macro2", "quote", - "syn 2.0.90", + "syn 2.0.114", ] [[package]] name = "pin-project-lite" -version = "0.2.15" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "915a1e146535de9163f3987b8944ed8cf49a18bb0056bcebcdcece385cece4ff" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" [[package]] name = "pin-utils" @@ -3292,9 +3344,9 @@ dependencies = [ [[package]] name = "pkg-config" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" [[package]] name = "plotters" @@ -3342,17 +3394,25 @@ dependencies = [ [[package]] name = "polling" -version = "3.7.4" +version = "3.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a604568c3202727d1507653cb121dbd627a58684eb09a820fd746bee38b4442f" +checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218" dependencies = [ "cfg-if", "concurrent-queue", - "hermit-abi 0.4.0", + "hermit-abi 0.5.2", "pin-project-lite", - "rustix 0.38.42", - "tracing", - "windows-sys 0.59.0", + "rustix 1.1.3", + "windows-sys 0.61.2", +] + +[[package]] +name = "potential_utf" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" +dependencies = [ + "zerovec", ] [[package]] @@ -3363,9 +3423,9 @@ checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" [[package]] name = "ppv-lite86" -version = "0.2.20" +version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" dependencies = [ "zerocopy", ] @@ -3399,19 +3459,19 @@ dependencies = [ [[package]] name = "prettyplease" -version = "0.2.25" +version = "0.2.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64d1ec885c64d0457d564db4ec299b2dae3f9c02808b8ad9c3a089c591b18033" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" dependencies = [ "proc-macro2", - "syn 2.0.90", + "syn 2.0.114", ] [[package]] name = "proc-macro-crate" -version = "3.2.0" +version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ecf48c7ca261d60b74ab1a7b20da18bede46776b2e55535cb958eb595c5fa7b" +checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" dependencies = [ "toml_edit", ] @@ -3442,9 +3502,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.95" +version = "1.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" dependencies = [ "unicode-ident", ] @@ -3475,7 +3535,7 @@ dependencies = [ "prost", "prost-types", "regex", - "syn 2.0.90", + "syn 2.0.114", "tempfile", ] @@ -3489,7 +3549,7 @@ dependencies = [ "itertools 0.14.0", "proc-macro2", "quote", - "syn 2.0.90", + "syn 2.0.114", ] [[package]] @@ -3525,7 +3585,7 @@ dependencies = [ "prost-reflect", "prost-types", "protox-parse", - "thiserror 2.0.6", + "thiserror 2.0.18", ] [[package]] @@ -3537,7 +3597,7 @@ dependencies = [ "logos", "miette", "prost-types", - "thiserror 2.0.6", + "thiserror 2.0.18", ] [[package]] @@ -3546,7 +3606,7 @@ version = "0.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f86ba2052aebccc42cbbb3ed234b8b13ce76f75c3551a303cb2bcffcff12bb14" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.10.0", "getopts", "memchr", "pulldown-cmark-escape", @@ -3561,33 +3621,33 @@ checksum = "007d8adb5ddab6f8e3f491ac63566a7d5002cc7ed73901f72057943fa71ae1ae" [[package]] name = "quick-junit" -version = "0.5.1" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ed1a693391a16317257103ad06a88c6529ac640846021da7c435a06fffdacd7" +checksum = "6ee9342d671fae8d66b3ae9fd7a9714dfd089c04d2a8b1ec0436ef77aee15e5f" dependencies = [ "chrono", - "indexmap 2.7.0", + "indexmap 2.13.0", "newtype-uuid", "quick-xml", "strip-ansi-escapes", - "thiserror 2.0.6", + "thiserror 2.0.18", "uuid", ] [[package]] name = "quick-xml" -version = "0.37.1" +version = "0.38.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f22f29bdff3987b4d8632ef95fd6424ec7e4e0a57e2f4fc63e489e75357f6a03" +checksum = "b66c2058c55a409d601666cffe35f04333cf1013010882cec174a7467cd4e21c" dependencies = [ "memchr", ] [[package]] name = "quote" -version = "1.0.37" +version = "1.0.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" +checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" dependencies = [ "proc-macro2", ] @@ -3625,14 +3685,14 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom 0.2.15", + "getrandom 0.2.17", ] [[package]] name = "rayon" -version = "1.10.0" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" +checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" dependencies = [ "either", "rayon-core", @@ -3640,9 +3700,9 @@ dependencies = [ [[package]] name = "rayon-core" -version = "1.12.1" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" dependencies = [ "crossbeam-deque", "crossbeam-utils", @@ -3656,11 +3716,20 @@ checksum = "41cc0f7e4d5d4544e8861606a285bb08d3e70712ccc7d2b84d7c0ccfaf4b05ce" [[package]] name = "redox_syscall" -version = "0.5.7" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags 2.10.0", +] + +[[package]] +name = "redox_syscall" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b6dfecf2c74bce2466cabf93f6664d6998a69eb21e39f4207930065b27b771f" +checksum = "49f3fe0889e69e2ae9e41f4d6c4c0181701d00e4697b356fb1f74173a5e0ee27" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.10.0", ] [[package]] @@ -3680,7 +3749,7 @@ version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" dependencies = [ - "getrandom 0.2.15", + "getrandom 0.2.17", "libredox", "thiserror 1.0.69", ] @@ -3693,17 +3762,8 @@ checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" dependencies = [ "aho-corasick", "memchr", - "regex-automata 0.4.13", - "regex-syntax 0.8.5", -] - -[[package]] -name = "regex-automata" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" -dependencies = [ - "regex-syntax 0.6.29", + "regex-automata", + "regex-syntax", ] [[package]] @@ -3714,20 +3774,14 @@ checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" dependencies = [ "aho-corasick", "memchr", - "regex-syntax 0.8.5", + "regex-syntax", ] [[package]] name = "regex-syntax" -version = "0.6.29" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" - -[[package]] -name = "regex-syntax" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" +checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" [[package]] name = "ring" @@ -3737,7 +3791,7 @@ checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" dependencies = [ "cc", "cfg-if", - "getrandom 0.2.15", + "getrandom 0.2.17", "libc", "untrusted", "windows-sys 0.52.0", @@ -3745,9 +3799,9 @@ dependencies = [ [[package]] name = "rsa" -version = "0.9.7" +version = "0.9.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47c75d7c5c6b673e58bf54d8544a9f432e3a925b0e80f7cd3602ab5c50c55519" +checksum = "b8573f03f5883dcaebdfcf4725caa1ecb9c15b2ef50c43a07b816e06799bb12d" dependencies = [ "const-oid", "digest", @@ -3793,9 +3847,9 @@ dependencies = [ [[package]] name = "rustc-demangle" -version = "0.1.24" +version = "0.1.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" +checksum = "b50b8869d9fc858ce7266cce0194bd74df58b9d0e3f6df3a9fc8eb470d95c09d" [[package]] name = "rustc-hash" @@ -3805,9 +3859,9 @@ checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" [[package]] name = "rustc-hash" -version = "2.1.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7fb8039b3032c191086b10f11f319a6e99e1e82889c5cc6046f515c9db1d497" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" [[package]] name = "rustc_version" @@ -3834,22 +3888,35 @@ dependencies = [ [[package]] name = "rustix" -version = "0.38.42" +version = "0.38.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f93dc38ecbab2eb790ff964bb77fa94faf256fd3e73285fd7ba0903b76bedb85" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.10.0", "errno", "libc", - "linux-raw-sys 0.4.14", + "linux-raw-sys 0.4.15", + "windows-sys 0.59.0", +] + +[[package]] +name = "rustix" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34" +dependencies = [ + "bitflags 2.10.0", + "errno", + "libc", + "linux-raw-sys 0.11.0", "windows-sys 0.59.0", ] [[package]] name = "rustls" -version = "0.23.31" +version = "0.23.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0ebcbd2f03de0fc1122ad9bb24b127a5a6cd51d72604a3f3c50ac459762b6cc" +checksum = "c665f33d38cea657d9614f766881e4d510e0eda4239891eea56b4cadcf01801b" dependencies = [ "log", "once_cell", @@ -3862,18 +3929,18 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.12.0" +version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79" +checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" dependencies = [ "zeroize", ] [[package]] name = "rustls-webpki" -version = "0.103.4" +version = "0.103.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a17884ae0c1b773f1ccd2bd4a8c72f16da897310a98b0e84bf349ad5ead92fc" +checksum = "d7df23109aa6c1567d1c575b9952556388da57401e4ace1d15f79eedad0d8f53" dependencies = [ "ring", "rustls-pki-types", @@ -3882,15 +3949,15 @@ dependencies = [ [[package]] name = "rustversion" -version = "1.0.20" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] name = "ryu" -version = "1.0.18" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" +checksum = "a50f4cf475b65d88e057964e0e9bb1f0aa9bbb2036dc65c64596b42932536984" [[package]] name = "same-file" @@ -3909,7 +3976,7 @@ checksum = "3fbf2ae1b8bc8e02df939598064d22402220cd5bbcca1c76f7d6a310974d5615" dependencies = [ "dyn-clone", "indexmap 1.9.3", - "indexmap 2.7.0", + "indexmap 2.13.0", "schemars_derive", "serde", "serde_json", @@ -3925,7 +3992,7 @@ dependencies = [ "proc-macro2", "quote", "serde_derive_internals", - "syn 2.0.90", + "syn 2.0.114", ] [[package]] @@ -3936,15 +4003,15 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "semver" -version = "1.0.26" +version = "1.0.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" [[package]] name = "serde" -version = "1.0.225" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd6c24dee235d0da097043389623fb913daddf92c76e9f5a1db88607a0bcbd1d" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" dependencies = [ "serde_core", "serde_derive", @@ -3952,22 +4019,22 @@ dependencies = [ [[package]] name = "serde_core" -version = "1.0.225" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "659356f9a0cb1e529b24c01e43ad2bdf520ec4ceaf83047b83ddcc2251f96383" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.225" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ea936adf78b1f766949a4977b91d2f5595825bd6ec079aa9543ad2685fc4516" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn 2.0.90", + "syn 2.0.114", ] [[package]] @@ -3978,41 +4045,41 @@ checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" dependencies = [ "proc-macro2", "quote", - "syn 2.0.90", + "syn 2.0.114", ] [[package]] name = "serde_json" -version = "1.0.145" +version = "1.0.149" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" dependencies = [ - "indexmap 2.7.0", + "indexmap 2.13.0", "itoa", "memchr", - "ryu", "serde", "serde_core", + "zmij", ] [[package]] name = "serde_repr" -version = "0.1.19" +version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9" +checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.90", + "syn 2.0.114", ] [[package]] name = "serde_spanned" -version = "0.6.8" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" +checksum = "f8bbf91e5a4d6315eee45e704372590b30e260ee83af6639d64557f51b067776" dependencies = [ - "serde", + "serde_core", ] [[package]] @@ -4049,9 +4116,9 @@ dependencies = [ [[package]] name = "sha2" -version = "0.10.8" +version = "0.10.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ "cfg-if", "cpufeatures", @@ -4075,10 +4142,11 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "signal-hook-registry" -version = "1.4.2" +version = "1.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" dependencies = [ + "errno", "libc", ] @@ -4092,6 +4160,12 @@ dependencies = [ "rand_core", ] +[[package]] +name = "simd-adler32" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" + [[package]] name = "simdutf8" version = "0.1.5" @@ -4100,9 +4174,9 @@ checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" [[package]] name = "similar" -version = "2.6.0" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1de1d4f81173b03af4c0cbed3c898f6bff5b870e4a7f5d6f4057d62a7a4b686e" +checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa" dependencies = [ "bstr", "unicode-segmentation", @@ -4110,18 +4184,15 @@ dependencies = [ [[package]] name = "slab" -version = "0.4.9" +version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" -dependencies = [ - "autocfg", -] +checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" [[package]] name = "slotmap" -version = "1.0.7" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbff4acf519f630b3a3ddcfaea6c06b42174d9a44bc70c620e9ed1649d58b82a" +checksum = "bdd58c3c93c3d278ca835519292445cb4b0d4dc59ccfdf7ceadaab3f8aeb4038" dependencies = [ "serde", "version_check", @@ -4129,9 +4200,9 @@ dependencies = [ [[package]] name = "smallvec" -version = "1.13.2" +version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" dependencies = [ "serde", ] @@ -4148,12 +4219,12 @@ dependencies = [ [[package]] name = "socket2" -version = "0.5.8" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c970269d99b64e60ec3bd6ad27270092a5394c4e309314b18ae3fe575695fbe8" +checksum = "86f4aa3ad99f2088c990dfa82d367e19cb29268ed67c574d10d0a4bfe71f07e0" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.60.2", ] [[package]] @@ -4175,21 +4246,11 @@ dependencies = [ "der", ] -[[package]] -name = "sqlformat" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7bba3a93db0cc4f7bdece8bb09e77e2e785c20bfebf79eb8340ed80708048790" -dependencies = [ - "nom", - "unicode_categories", -] - [[package]] name = "sqlx" -version = "0.8.2" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93334716a037193fac19df402f8571269c84a00852f6a7066b5d2616dcd64d3e" +checksum = "1fefb893899429669dcdd979aff487bd78f4064e5e7907e4269081e0ef7d97dc" dependencies = [ "sqlx-core", "sqlx-macros", @@ -4200,39 +4261,34 @@ dependencies = [ [[package]] name = "sqlx-core" -version = "0.8.2" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4d8060b456358185f7d50c55d9b5066ad956956fddec42ee2e8567134a8936e" +checksum = "ee6798b1838b6a0f69c007c133b8df5866302197e404e8b6ee8ed3e3a5e68dc6" dependencies = [ "async-io 1.13.0", "async-std", - "atoi", - "byteorder", + "base64 0.22.1", "bytes", "crc", "crossbeam-queue", "either", - "event-listener 5.3.1", - "futures-channel", + "event-listener 5.4.1", "futures-core", "futures-intrusive", "futures-io", "futures-util", - "hashbrown 0.14.5", + "hashbrown 0.15.5", "hashlink", - "hex", - "indexmap 2.7.0", + "indexmap 2.13.0", "log", "memchr", "once_cell", - "paste", "percent-encoding", "serde", "serde_json", "sha2", "smallvec", - "sqlformat", - "thiserror 1.0.69", + "thiserror 2.0.18", "tokio", "tokio-stream", "tracing", @@ -4241,22 +4297,22 @@ dependencies = [ [[package]] name = "sqlx-macros" -version = "0.8.2" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cac0692bcc9de3b073e8d747391827297e075c7710ff6276d9f7a1f3d58c6657" +checksum = "a2d452988ccaacfbf5e0bdbc348fb91d7c8af5bee192173ac3636b5fb6e6715d" dependencies = [ "proc-macro2", "quote", "sqlx-core", "sqlx-macros-core", - "syn 2.0.90", + "syn 2.0.114", ] [[package]] name = "sqlx-macros-core" -version = "0.8.2" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1804e8a7c7865599c9c79be146dc8a9fd8cc86935fa641d3ea58e5f0688abaa5" +checksum = "19a9c1841124ac5a61741f96e1d9e2ec77424bf323962dd894bdb93f37d5219b" dependencies = [ "async-std", "dotenvy", @@ -4273,21 +4329,20 @@ dependencies = [ "sqlx-mysql", "sqlx-postgres", "sqlx-sqlite", - "syn 2.0.90", - "tempfile", + "syn 2.0.114", "tokio", "url", ] [[package]] name = "sqlx-mysql" -version = "0.8.2" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64bb4714269afa44aef2755150a0fc19d756fb580a67db8885608cf02f47d06a" +checksum = "aa003f0038df784eb8fecbbac13affe3da23b45194bd57dba231c8f48199c526" dependencies = [ "atoi", "base64 0.22.1", - "bitflags 2.6.0", + "bitflags 2.10.0", "byteorder", "bytes", "crc", @@ -4316,27 +4371,26 @@ dependencies = [ "smallvec", "sqlx-core", "stringprep", - "thiserror 1.0.69", + "thiserror 2.0.18", "tracing", "whoami", ] [[package]] name = "sqlx-postgres" -version = "0.8.2" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fa91a732d854c5d7726349bb4bb879bb9478993ceb764247660aee25f67c2f8" +checksum = "db58fcd5a53cf07c184b154801ff91347e4c30d17a3562a635ff028ad5deda46" dependencies = [ "atoi", "base64 0.22.1", - "bitflags 2.6.0", + "bitflags 2.10.0", "byteorder", "crc", "dotenvy", "etcetera", "futures-channel", "futures-core", - "futures-io", "futures-util", "hex", "hkdf", @@ -4354,16 +4408,16 @@ dependencies = [ "smallvec", "sqlx-core", "stringprep", - "thiserror 1.0.69", + "thiserror 2.0.18", "tracing", "whoami", ] [[package]] name = "sqlx-sqlite" -version = "0.8.2" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5b2cf34a45953bfd3daaf3db0f7a7878ab9b7a6b91b422d24a7a9e4c857b680" +checksum = "c2d12fe70b2c1b4401038055f90f151b78208de1f9f89a7dbfd41587a10c3eea" dependencies = [ "atoi", "flume", @@ -4378,15 +4432,16 @@ dependencies = [ "serde", "serde_urlencoded", "sqlx-core", + "thiserror 2.0.18", "tracing", "url", ] [[package]] name = "stable_deref_trait" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" [[package]] name = "static_assertions" @@ -4413,9 +4468,9 @@ dependencies = [ [[package]] name = "strip-ansi-escapes" -version = "0.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55ff8ef943b384c414f54aefa961dd2bd853add74ec75e7ac74cf91dba62bcfa" +checksum = "2a8f8038e7e7969abb3f1b7c2a811225e9296da208539e0f79c5251d6cac0025" dependencies = [ "vte", ] @@ -4428,24 +4483,23 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "strum" -version = "0.27.1" +version = "0.27.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f64def088c51c9510a8579e3c5d67c65349dcf755e5479ad3d010aa6454e2c32" +checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf" dependencies = [ "strum_macros", ] [[package]] name = "strum_macros" -version = "0.27.1" +version = "0.27.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c77a8c5abcaf0f9ce05d62342b7d298c346515365c36b673df4ebe3ced01fde8" +checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7" dependencies = [ "heck", "proc-macro2", "quote", - "rustversion", - "syn 2.0.90", + "syn 2.0.114", ] [[package]] @@ -4476,9 +4530,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.90" +version = "2.0.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "919d3b74a5dd0ccd15aeb8f93e7006bd9e14c295087c9896a110f490752bcf31" +checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a" dependencies = [ "proc-macro2", "quote", @@ -4487,32 +4541,31 @@ dependencies = [ [[package]] name = "synstructure" -version = "0.13.1" +version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.90", + "syn 2.0.114", ] [[package]] name = "target-triple" -version = "0.1.3" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42a4d50cdb458045afc8131fd91b64904da29548bcb63c7236e0844936c13078" +checksum = "591ef38edfb78ca4771ee32cf494cb8771944bee237a9b91fc9c1424ac4b777b" [[package]] name = "tempfile" -version = "3.15.0" +version = "3.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a8a559c81686f576e8cd0290cd2a24a2a9ad80c98b3478856500fcbd7acd704" +checksum = "655da9c7eb6305c55742045d5a8d2037996d61d8de95806335c7c86ce0f82e9c" dependencies = [ - "cfg-if", "fastrand 2.3.0", - "getrandom 0.2.15", + "getrandom 0.3.4", "once_cell", - "rustix 0.38.42", + "rustix 1.1.3", "windows-sys 0.59.0", ] @@ -4544,9 +4597,9 @@ checksum = "8f50febec83f5ee1df3015341d8bd429f2d1cc62bcba7ea2076759d315084683" [[package]] name = "test-log" -version = "0.2.17" +version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7f46083d221181166e5b6f6b1e5f1d499f3a76888826e6cb1d057554157cd0f" +checksum = "37d53ac171c92a39e4769491c4b4dde7022c60042254b5fc044ae409d34a24d4" dependencies = [ "env_logger", "test-log-macros", @@ -4555,13 +4608,13 @@ dependencies = [ [[package]] name = "test-log-macros" -version = "0.2.17" +version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "888d0c3c6db53c0fdab160d2ed5e12ba745383d3e85813f2ea0f2b1475ab553f" +checksum = "be35209fd0781c5401458ab66e4f98accf63553e8fae7425503e92fdd319783b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.90", + "syn 2.0.114", ] [[package]] @@ -4575,11 +4628,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.6" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fec2a1820ebd077e2b90c4df007bebf344cd394098a13c563957d0afc83ea47" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" dependencies = [ - "thiserror-impl 2.0.6", + "thiserror-impl 2.0.18", ] [[package]] @@ -4590,35 +4643,34 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.90", + "syn 2.0.114", ] [[package]] name = "thiserror-impl" -version = "2.0.6" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d65750cab40f4ff1929fb1ba509e9914eb756131cef4210da8d5d700d26f6312" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.90", + "syn 2.0.114", ] [[package]] name = "thread_local" -version = "1.1.8" +version = "1.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" dependencies = [ "cfg-if", - "once_cell", ] [[package]] name = "tikv-jemalloc-sys" -version = "0.6.0+5.3.0-1-ge13ca993e8ccb9ba9847cc330696e02839f328f7" +version = "0.6.1+5.3.0-1-ge13ca993e8ccb9ba9847cc330696e02839f328f7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd3c60906412afa9c2b5b5a48ca6a5abe5736aec9eb48ad05037a677e52e4e2d" +checksum = "cd8aa5b2ab86a2cefa406d889139c162cbb230092f7d1d7cbc1716405d852a3b" dependencies = [ "cc", "libc", @@ -4626,9 +4678,9 @@ dependencies = [ [[package]] name = "tikv-jemallocator" -version = "0.6.0" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cec5ff18518d81584f477e9bfdf957f5bb0979b0bac3af4ca30b5b3ae2d2865" +checksum = "0359b4327f954e0567e69fb191cf1436617748813819c94b8cd4a431422d053a" dependencies = [ "libc", "tikv-jemalloc-sys", @@ -4636,9 +4688,9 @@ dependencies = [ [[package]] name = "time" -version = "0.3.37" +version = "0.3.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35e7868883861bd0e56d9ac6efcaaca0d6d5d82a2a7ec8209ff492c07cf37b21" +checksum = "9da98b7d9b7dad93488a84b8248efc35352b0b2657397d4167e7ad67e5d535e5" dependencies = [ "deranged", "itoa", @@ -4646,22 +4698,22 @@ dependencies = [ "num-conv", "num_threads", "powerfmt", - "serde", + "serde_core", "time-core", "time-macros", ] [[package]] name = "time-core" -version = "0.1.2" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" +checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" [[package]] name = "time-macros" -version = "0.2.19" +version = "0.2.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2834e6017e3e5e4b9834939793b282bc03b37a3336245fa820e35e233e2a85de" +checksum = "78cc610bac2dcee56805c99642447d4c5dbde4d01f752ffea0199aee1f601dc4" dependencies = [ "num-conv", "time-core", @@ -4669,9 +4721,9 @@ dependencies = [ [[package]] name = "tinystr" -version = "0.7.6" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" +checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" dependencies = [ "displaydoc", "zerovec", @@ -4689,9 +4741,9 @@ dependencies = [ [[package]] name = "tinyvec" -version = "1.8.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "445e881f4f6d382d5f27c034e25eb92edd7c784ceab92a0937db7f2e9471b938" +checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" dependencies = [ "tinyvec_macros", ] @@ -4704,38 +4756,37 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.42.0" +version = "1.49.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cec9b21b0450273377fc97bd4c33a8acffc8c996c987a7c5b319a0083707551" +checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86" dependencies = [ - "backtrace", "bytes", "libc", "mio", "parking_lot", "pin-project-lite", "signal-hook-registry", - "socket2 0.5.8", + "socket2 0.6.2", "tokio-macros", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] name = "tokio-macros" -version = "2.4.0" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" +checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.90", + "syn 2.0.114", ] [[package]] name = "tokio-stream" -version = "0.1.17" +version = "0.1.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047" +checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70" dependencies = [ "futures-core", "pin-project-lite", @@ -4744,9 +4795,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.13" +version = "0.7.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7fcaa8d55a2bdd6b83ace262b016eca0d79ee02818c5c1bcdf0305114081078" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" dependencies = [ "bytes", "futures-core", @@ -4757,38 +4808,55 @@ dependencies = [ [[package]] name = "toml" -version = "0.8.19" +version = "0.9.11+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1ed1f98e3fdc28d6d910e6737ae6ab1a93bf1985935a1193e68f93eeb68d24e" +checksum = "f3afc9a848309fe1aaffaed6e1546a7a14de1f935dc9d89d32afd9a44bab7c46" dependencies = [ - "serde", + "indexmap 2.13.0", + "serde_core", "serde_spanned", "toml_datetime", - "toml_edit", + "toml_parser", + "toml_writer", + "winnow", ] [[package]] name = "toml_datetime" -version = "0.6.8" +version = "0.7.5+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" +checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" dependencies = [ - "serde", + "serde_core", ] [[package]] name = "toml_edit" -version = "0.22.22" +version = "0.23.10+spec-1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5" +checksum = "84c8b9f757e028cee9fa244aea147aab2a9ec09d5325a9b01e0a49730c2b5269" dependencies = [ - "indexmap 2.7.0", - "serde", - "serde_spanned", + "indexmap 2.13.0", "toml_datetime", + "toml_parser", + "winnow", +] + +[[package]] +name = "toml_parser" +version = "1.0.6+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3198b4b0a8e11f09dd03e133c0280504d0801269e9afa46362ffde1cbeebf44" +dependencies = [ "winnow", ] +[[package]] +name = "toml_writer" +version = "1.0.6+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab16f14aed21ee8bfd8ec22513f7287cd4a91aa92e44edfe2c17ddd004e92607" + [[package]] name = "tower" version = "0.4.13" @@ -4823,7 +4891,7 @@ dependencies = [ "dashmap 5.5.3", "futures", "httparse", - "lsp-types", + "lsp-types 0.94.1", "memchr", "serde", "serde_json", @@ -4842,7 +4910,7 @@ checksum = "84fd902d4e0b9a4b27f2f440108dc034e1758628a9b702f8ec61ad66355422fa" dependencies = [ "proc-macro2", "quote", - "syn 2.0.90", + "syn 2.0.114", ] [[package]] @@ -4853,9 +4921,9 @@ checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" [[package]] name = "tracing" -version = "0.1.41" +version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" dependencies = [ "log", "pin-project-lite", @@ -4865,25 +4933,25 @@ dependencies = [ [[package]] name = "tracing-appender" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3566e8ce28cc0a3fe42519fc80e6b4c943cc4c8cef275620eb8dac2d3d4e06cf" +checksum = "786d480bce6247ab75f005b14ae1624ad978d3029d9113f0a22fa1ac773faeaf" dependencies = [ "crossbeam-channel", - "thiserror 1.0.69", + "thiserror 2.0.18", "time", "tracing-subscriber", ] [[package]] name = "tracing-attributes" -version = "0.1.28" +version = "0.1.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.90", + "syn 2.0.114", ] [[package]] @@ -4906,9 +4974,9 @@ dependencies = [ [[package]] name = "tracing-core" -version = "0.1.33" +version = "0.1.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" dependencies = [ "once_cell", "valuable", @@ -4948,14 +5016,14 @@ dependencies = [ [[package]] name = "tracing-subscriber" -version = "0.3.19" +version = "0.3.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008" +checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e" dependencies = [ "matchers", - "nu-ansi-term 0.46.0", + "nu-ansi-term", "once_cell", - "regex", + "regex-automata", "serde", "serde_json", "sharded-slab", @@ -4969,11 +5037,11 @@ dependencies = [ [[package]] name = "tracing-tree" -version = "0.4.0" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f459ca79f1b0d5f71c54ddfde6debfc59c8b6eeb46808ae492077f739dc7b49c" +checksum = "ac87aa03b6a4d5a7e4810d1a80c19601dbe0f8a837e9177f23af721c7ba7beec" dependencies = [ - "nu-ansi-term 0.50.1", + "nu-ansi-term", "time", "tracing-core", "tracing-log 0.2.0", @@ -4982,13 +5050,13 @@ dependencies = [ [[package]] name = "tree-sitter" -version = "0.25.9" +version = "0.25.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccd2a058a86cfece0bf96f7cce1021efef9c8ed0e892ab74639173e5ed7a34fa" +checksum = "78f873475d258561b06f1c595d93308a7ed124d9977cb26b148c2084a4a3cc87" dependencies = [ "cc", "regex", - "regex-syntax 0.8.5", + "regex-syntax", "serde_json", "streaming-iterator", "tree-sitter-language", @@ -4996,15 +5064,15 @@ dependencies = [ [[package]] name = "tree-sitter-language" -version = "0.1.5" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4013970217383f67b18aef68f6fb2e8d409bc5755227092d32efb0422ba24b8" +checksum = "4ae62f7eae5eb549c71b76658648b72cc6111f2d87d24a1e31fa907f4943e3ce" [[package]] name = "trybuild" -version = "1.0.101" +version = "1.0.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8dcd332a5496c026f1e14b7f3d2b7bd98e509660c04239c58b0ba38a12daded4" +checksum = "3e17e807bff86d2a06b52bca4276746584a78375055b6e45843925ce2802b335" dependencies = [ "glob", "serde", @@ -5017,21 +5085,21 @@ dependencies = [ [[package]] name = "typenum" -version = "1.17.0" +version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" [[package]] name = "unicase" -version = "2.8.0" +version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e51b68083f157f853b6379db119d1c1be0e6e4dec98101079dec41f6f5cf6df" +checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" [[package]] name = "unicode-bidi" -version = "0.3.17" +version = "0.3.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ab17db44d7388991a428b2ee655ce0c212e862eff1768a455c58f9aad6e7893" +checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" [[package]] name = "unicode-bom" @@ -5041,24 +5109,24 @@ checksum = "7eec5d1121208364f6793f7d2e222bf75a915c19557537745b195b253dd64217" [[package]] name = "unicode-ident" -version = "1.0.14" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" +checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" [[package]] name = "unicode-normalization" -version = "0.1.24" +version = "0.1.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5033c97c4262335cded6d6fc3e5c18ab755e1a3dc96376350f3d8e9f009ad956" +checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8" dependencies = [ "tinyvec", ] [[package]] name = "unicode-properties" -version = "0.1.3" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e70f2a8b45122e719eb623c01822704c4e0907e7e426a05927e1a1cfff5b75d0" +checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" [[package]] name = "unicode-segmentation" @@ -5073,10 +5141,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" [[package]] -name = "unicode_categories" -version = "0.1.1" +name = "unicode-width" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" [[package]] name = "unindent" @@ -5108,22 +5176,17 @@ dependencies = [ [[package]] name = "url" -version = "2.5.4" +version = "2.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" dependencies = [ "form_urlencoded", "idna", "percent-encoding", "serde", + "serde_derive", ] -[[package]] -name = "utf16_iter" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" - [[package]] name = "utf8_iter" version = "1.0.4" @@ -5138,26 +5201,26 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.18.1" +version = "1.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2" +checksum = "ee48d38b119b0cd71fe4141b30f5ba9c7c5d9f4e7a3a8b4a674e4b6ef789976f" dependencies = [ - "getrandom 0.3.3", + "getrandom 0.3.4", "js-sys", "wasm-bindgen", ] [[package]] name = "valuable" -version = "0.1.0" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" [[package]] name = "value-bag" -version = "1.10.0" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ef4c4aa54d5d05a279399bfa921ec387b7aba77caf7a682ae8d86785b8fdad2" +checksum = "7ba6f5989077681266825251a52748b8c1d8a4ad098cc37e440103d0ea717fc0" [[package]] name = "vcpkg" @@ -5173,22 +5236,11 @@ checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] name = "vte" -version = "0.11.1" +version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5022b5fbf9407086c180e9557be968742d839e68346af7792b8592489732197" +checksum = "231fdcd7ef3037e8330d8e17e61011a2c244126acc0a982f4040ac3f9f0bc077" dependencies = [ - "utf8parse", - "vte_generate_state_changes", -] - -[[package]] -name = "vte_generate_state_changes" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e369bee1b05d510a7b4ed645f5faa90619e05437111783ea5848f28d97d3c2e" -dependencies = [ - "proc-macro2", - "quote", + "memchr", ] [[package]] @@ -5224,24 +5276,15 @@ checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" [[package]] name = "wasi" -version = "0.11.0+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" - -[[package]] -name = "wasi" -version = "0.14.7+wasi-0.2.4" +version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "883478de20367e224c0090af9cf5f9fa85bed63a95c1abf3afc5c083ebc06e8c" -dependencies = [ - "wasip2", -] +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasip2" -version = "1.0.1+wasi-0.2.4" +version = "1.0.2+wasi-0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" dependencies = [ "wit-bindgen", ] @@ -5254,9 +5297,9 @@ checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" [[package]] name = "wasm-bindgen" -version = "0.2.104" +version = "0.2.108" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1da10c01ae9f1ae40cbfac0bac3b1e724b320abfcf52229f80b547c0d250e2d" +checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566" dependencies = [ "cfg-if", "once_cell", @@ -5265,27 +5308,14 @@ dependencies = [ "wasm-bindgen-shared", ] -[[package]] -name = "wasm-bindgen-backend" -version = "0.2.104" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "671c9a5a66f49d8a47345ab942e2cb93c7d1d0339065d4f8139c486121b43b19" -dependencies = [ - "bumpalo", - "log", - "proc-macro2", - "quote", - "syn 2.0.90", - "wasm-bindgen-shared", -] - [[package]] name = "wasm-bindgen-futures" -version = "0.4.54" +version = "0.4.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e038d41e478cc73bae0ff9b36c60cff1c98b8f38f8d7e8061e79ee63608ac5c" +checksum = "70a6e77fd0ae8029c9ea0063f87c46fde723e7d887703d74ad2616d792e51e6f" dependencies = [ "cfg-if", + "futures-util", "js-sys", "once_cell", "wasm-bindgen", @@ -5294,9 +5324,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.104" +version = "0.2.108" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ca60477e4c59f5f2986c50191cd972e3a50d8a95603bc9434501cf156a9a119" +checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -5304,31 +5334,31 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.104" +version = "0.2.108" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f07d2f20d4da7b26400c9f4a0511e6e0345b040694e8a75bd41d578fa4421d7" +checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55" dependencies = [ + "bumpalo", "proc-macro2", "quote", - "syn 2.0.90", - "wasm-bindgen-backend", + "syn 2.0.114", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.104" +version = "0.2.108" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bad67dc8b2a1a6e5448428adec4c3e84c43e561d8c9ee8a9e5aabeb193ec41d1" +checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12" dependencies = [ "unicode-ident", ] [[package]] name = "web-sys" -version = "0.3.81" +version = "0.3.85" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9367c417a924a74cae129e6a2ae3b47fabb1f8995595ab474029da749a8be120" +checksum = "312e32e551d92129218ea9a2452120f4aabc03529ef03e4d0d82fb2780608598" dependencies = [ "js-sys", "wasm-bindgen", @@ -5340,14 +5370,14 @@ version = "0.26.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" dependencies = [ - "webpki-roots 1.0.2", + "webpki-roots 1.0.5", ] [[package]] name = "webpki-roots" -version = "1.0.2" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e8983c3ab33d6fb807cfcdad2491c4ea8cbc8ed839181c7dfd9c67c83e261b2" +checksum = "12bed680863276c63889429bfd6cab3b99943659923822de1c8a39c49e4d722c" dependencies = [ "rustls-pki-types", ] @@ -5360,17 +5390,17 @@ checksum = "b4ee928febd44d98f2f459a4a79bd4d928591333a494a10a868418ac1b39cf1f" dependencies = [ "either", "home", - "rustix 0.38.42", + "rustix 0.38.44", "winsafe", ] [[package]] name = "whoami" -version = "1.5.2" +version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "372d5b87f58ec45c384ba03563b03544dc5fadc3983e434b286913f5b4a9bb6d" +checksum = "5d4a4db5077702ca3015d3d02d74974948aba2ad9e12ab7df718ee64ccd7e97d" dependencies = [ - "redox_syscall 0.5.7", + "libredox", "wasite", ] @@ -5392,11 +5422,11 @@ checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-util" -version = "0.1.9" +version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.48.0", ] [[package]] @@ -5405,6 +5435,12 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + [[package]] name = "windows-sys" version = "0.48.0" @@ -5432,6 +5468,24 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + [[package]] name = "windows-targets" version = "0.48.5" @@ -5456,13 +5510,30 @@ dependencies = [ "windows_aarch64_gnullvm 0.52.6", "windows_aarch64_msvc 0.52.6", "windows_i686_gnu 0.52.6", - "windows_i686_gnullvm", + "windows_i686_gnullvm 0.52.6", "windows_i686_msvc 0.52.6", "windows_x86_64_gnu 0.52.6", "windows_x86_64_gnullvm 0.52.6", "windows_x86_64_msvc 0.52.6", ] +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + [[package]] name = "windows_aarch64_gnullvm" version = "0.48.5" @@ -5475,6 +5546,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + [[package]] name = "windows_aarch64_msvc" version = "0.48.5" @@ -5487,6 +5564,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + [[package]] name = "windows_i686_gnu" version = "0.48.5" @@ -5499,12 +5582,24 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + [[package]] name = "windows_i686_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + [[package]] name = "windows_i686_msvc" version = "0.48.5" @@ -5517,6 +5612,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + [[package]] name = "windows_x86_64_gnu" version = "0.48.5" @@ -5529,6 +5630,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + [[package]] name = "windows_x86_64_gnullvm" version = "0.48.5" @@ -5541,6 +5648,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + [[package]] name = "windows_x86_64_msvc" version = "0.48.5" @@ -5553,11 +5666,17 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + [[package]] name = "winnow" -version = "0.6.20" +version = "0.7.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36c1fec1a2bb5866f07c25f68c26e565c4c200aebb96d7e55710c19d3e8ac49b" +checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" dependencies = [ "memchr", ] @@ -5570,9 +5689,9 @@ checksum = "d135d17ab770252ad95e9a872d365cf3090e3be864a34ab46f48555993efc904" [[package]] name = "wit-bindgen" -version = "0.46.0" +version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" [[package]] name = "write-json" @@ -5580,17 +5699,11 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "23f6174b2566cc4a74f95e1367ec343e7fa80c93cc8087f5c4a3d6a1088b2118" -[[package]] -name = "write16" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" - [[package]] name = "writeable" -version = "0.5.5" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" +checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" [[package]] name = "xflags" @@ -5662,11 +5775,10 @@ dependencies = [ [[package]] name = "yoke" -version = "0.7.5" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40" +checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" dependencies = [ - "serde", "stable_deref_trait", "yoke-derive", "zerofrom", @@ -5674,69 +5786,79 @@ dependencies = [ [[package]] name = "yoke-derive" -version = "0.7.5" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" +checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.90", + "syn 2.0.114", "synstructure", ] [[package]] name = "zerocopy" -version = "0.7.35" +version = "0.8.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" +checksum = "71ddd76bcebeed25db614f82bf31a9f4222d3fbba300e6fb6c00afa26cbd4d9d" dependencies = [ - "byteorder", "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.7.35" +version = "0.8.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" +checksum = "d8187381b52e32220d50b255276aa16a084ec0a9017a0ca2152a1f55c539758d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.90", + "syn 2.0.114", ] [[package]] name = "zerofrom" -version = "0.1.5" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cff3ee08c995dee1859d998dea82f7374f2826091dd9cd47def953cae446cd2e" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" dependencies = [ "zerofrom-derive", ] [[package]] name = "zerofrom-derive" -version = "0.1.5" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "595eed982f7d355beb85837f651fa22e90b3c044842dc7f2c2842c086f295808" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote", - "syn 2.0.90", + "syn 2.0.114", "synstructure", ] [[package]] name = "zeroize" -version = "1.8.1" +version = "1.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" + +[[package]] +name = "zerotrie" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] [[package]] name = "zerovec" -version = "0.10.4" +version = "0.11.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" +checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" dependencies = [ "yoke", "zerofrom", @@ -5745,13 +5867,13 @@ dependencies = [ [[package]] name = "zerovec-derive" -version = "0.10.3" +version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" +checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.90", + "syn 2.0.114", ] [[package]] @@ -5766,3 +5888,9 @@ dependencies = [ "flate2", "time", ] + +[[package]] +name = "zmij" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02aae0f83f69aafc94776e879363e9771d7ecbffe2c7fbb6c14c5e00dfe88439" diff --git a/crates/pgls_pglinter/src/cache.rs b/crates/pgls_pglinter/src/cache.rs index f8739e4d5..ffa0dbbd3 100644 --- a/crates/pgls_pglinter/src/cache.rs +++ b/crates/pgls_pglinter/src/cache.rs @@ -1,9 +1,31 @@ //! Pglinter extension cache for avoiding repeated database queries use pgls_schema_cache::SchemaCache; -use rustc_hash::FxHashSet; +use rustc_hash::{FxHashMap, FxHashSet}; use sqlx::PgPool; +/// Rule message from pglinter.rule_messages table (pglinter v1.1.0+) +#[derive(Debug, Clone, Default)] +pub struct RuleMessage { + /// Severity level (e.g., "WARNING", "ERROR") + pub severity: String, + /// Message template with {object} placeholder + pub message: String, + /// Detailed advice/description + pub advices: String, + /// List of fix suggestions + pub infos: Vec, +} + +/// Raw JSON structure from pglinter.rule_messages +#[derive(Debug, serde::Deserialize)] +struct RuleMessageJson { + severity: Option, + message: Option, + advices: Option, + infos: Option>, +} + /// Cached pglinter extension state (loaded once, reused) #[derive(Debug, Clone, Default)] pub struct PglinterCache { @@ -11,6 +33,8 @@ pub struct PglinterCache { pub extension_installed: bool, /// Rule codes that are disabled in the pglinter extension pub disabled_rules: FxHashSet, + /// Rule messages from pglinter.rule_messages table (pglinter v1.1.0+) + pub rule_messages: FxHashMap, } impl PglinterCache { @@ -22,15 +46,24 @@ impl PglinterCache { return Ok(Self { extension_installed: false, disabled_rules: FxHashSet::default(), + rule_messages: FxHashMap::default(), }); } - // Get disabled rules using pglinter.show_rules() - single query + // Get disabled rules using pglinter.rules table let disabled_rules = get_disabled_rules(conn).await?; + // Get rule messages if pglinter v1.1.0+ (rule_messages table exists) + let rule_messages = if check_rule_messages_table_exists(conn).await? { + fetch_rule_messages(conn).await? + } else { + FxHashMap::default() + }; + Ok(Self { extension_installed, disabled_rules, + rule_messages, }) } @@ -39,6 +72,7 @@ impl PglinterCache { Self { extension_installed: schema_cache.extensions.iter().any(|e| e.name == "pglinter"), disabled_rules: FxHashSet::default(), + rule_messages: FxHashMap::default(), } } } @@ -56,3 +90,40 @@ pub async fn get_disabled_rules(conn: &PgPool) -> Result, sqlx .map(|(code, _)| code) .collect()) } + +/// Check if pglinter.rule_messages table exists (requires pglinter v1.1.0+) +pub async fn check_rule_messages_table_exists(conn: &PgPool) -> Result { + let result: Option<(bool,)> = sqlx::query_as( + "SELECT EXISTS ( + SELECT 1 FROM information_schema.tables + WHERE table_schema = 'pglinter' AND table_name = 'rule_messages' + )", + ) + .fetch_optional(conn) + .await?; + + Ok(result.map(|(exists,)| exists).unwrap_or(false)) +} + +/// Fetch rule messages from pglinter.rule_messages table (pglinter v1.1.0+) +pub async fn fetch_rule_messages( + conn: &PgPool, +) -> Result, sqlx::Error> { + let rows: Vec<(String, sqlx::types::Json)> = + sqlx::query_as("SELECT code, rule_msg FROM pglinter.rule_messages") + .fetch_all(conn) + .await?; + + Ok(rows + .into_iter() + .map(|(code, json)| { + let msg = RuleMessage { + severity: json.severity.clone().unwrap_or_default(), + message: json.message.clone().unwrap_or_default(), + advices: json.advices.clone().unwrap_or_default(), + infos: json.infos.clone().unwrap_or_default(), + }; + (code, msg) + }) + .collect()) +} diff --git a/crates/pgls_pglinter/src/diagnostics.rs b/crates/pgls_pglinter/src/diagnostics.rs index 47753951f..f0fa72cda 100644 --- a/crates/pgls_pglinter/src/diagnostics.rs +++ b/crates/pgls_pglinter/src/diagnostics.rs @@ -1,5 +1,6 @@ //! Pglinter diagnostic types +use crate::cache::RuleMessage; use pgls_diagnostics::{ Advices, Category, DatabaseObjectOwned, Diagnostic, LogCategory, MessageAndDescription, Severity, Visit, @@ -52,12 +53,12 @@ impl Advices for PglinterAdvices { visitor.record_log(LogCategory::Info, &format!("Rule: {code}"))?; } - if let Some(objects) = &self.object_list { - if !objects.is_empty() { - visitor.record_log(LogCategory::None, &"Affected objects:")?; - for line in objects.lines() { - visitor.record_log(LogCategory::Info, &format!(" {line}"))?; - } + if let Some(objects) = &self.object_list + && !objects.is_empty() + { + visitor.record_log(LogCategory::None, &"Affected objects:")?; + for line in objects.lines() { + visitor.record_log(LogCategory::Info, &format!(" {line}"))?; } } @@ -135,27 +136,58 @@ impl PglinterDiagnostic { }) } - /// Create diagnostic from a pglinter violation with optional object info + /// Create diagnostic from a pglinter violation with optional object info and rule message pub fn from_violation( rule_code: &str, db_object: Option, + rule_message: Option<&RuleMessage>, ) -> Option { let category = crate::registry::get_rule_category(rule_code)?; let metadata = crate::registry::get_rule_metadata_by_code(rule_code)?; - let fixes: Vec = metadata.fixes.iter().map(|s| s.to_string()).collect(); + // Get object name for placeholder replacement + let obj_name = db_object + .as_ref() + .map(|obj| { + if let Some(ref schema) = obj.schema { + format!("'{}.{}'", schema, obj.name) + } else { + format!("'{}'", obj.name) + } + }) + .unwrap_or_else(|| "Object".to_string()); - // Generate a violation-specific message - let message = violation_message(rule_code, db_object.as_ref()); + // Use rule_message from pglinter v1.1.0+ if available, otherwise fall back to hardcoded + let (message, advice_description, fixes) = if let Some(rm) = rule_message { + // Replace {object} placeholder with actual object name + let msg = rm.message.replace("{object}", &obj_name); + let advice = rm.advices.clone(); + let fix_list = rm.infos.clone(); + (msg, advice, fix_list) + } else { + // Fall back to hardcoded messages for older pglinter versions + let msg = violation_message(rule_code, db_object.as_ref()); + let advice = violation_advice(rule_code); + let fix_list = metadata.fixes.iter().map(|s| s.to_string()).collect(); + (msg, advice, fix_list) + }; - // Generate a violation-specific advice (more detailed explanation) - let advice_description = violation_advice(rule_code); + // Determine severity from rule_message or default to Warning + let severity = rule_message + .map(|rm| match rm.severity.to_uppercase().as_str() { + "ERROR" => Severity::Error, + "WARNING" => Severity::Warning, + "INFO" | "INFORMATION" => Severity::Information, + "HINT" => Severity::Hint, + _ => Severity::Warning, + }) + .unwrap_or(Severity::Warning); Some(PglinterDiagnostic { category, db_object, message: message.into(), - severity: Severity::Warning, + severity, advices: PglinterAdvices { description: advice_description, rule_code: Some(rule_code.to_string()), diff --git a/crates/pgls_pglinter/src/lib.rs b/crates/pgls_pglinter/src/lib.rs index 95b3e0542..876a940db 100644 --- a/crates/pgls_pglinter/src/lib.rs +++ b/crates/pgls_pglinter/src/lib.rs @@ -9,9 +9,10 @@ pub mod rules; use pgls_analyse::{AnalysisFilter, RegistryVisitor, RuleMeta}; use pgls_diagnostics::DatabaseObjectOwned; use pgls_schema_cache::SchemaCache; +use rustc_hash::FxHashMap; use sqlx::PgPool; -pub use cache::PglinterCache; +pub use cache::{PglinterCache, RuleMessage}; pub use diagnostics::{PglinterAdvices, PglinterDiagnostic}; pub use rule::PglinterRule; @@ -60,10 +61,10 @@ impl<'a> RegistryVisitor for RuleCollector<'a> { } fn record_rule(&mut self) { - if self.filter.match_rule::() { - if let Some(code) = registry::get_rule_code(R::METADATA.name) { - self.enabled_rules.push(code.to_string()); - } + if self.filter.match_rule::() + && let Some(code) = registry::get_rule_code(R::METADATA.name) + { + self.enabled_rules.push(code.to_string()); } } } @@ -128,6 +129,18 @@ pub async fn run_pglinter( return Ok(results); } + // Get rule messages from cache or fetch + let rule_messages = match cache { + Some(c) => c.rule_messages.clone(), + None => { + if cache::check_rule_messages_table_exists(params.conn).await? { + cache::fetch_rule_messages(params.conn).await? + } else { + FxHashMap::default() + } + } + }; + // Fetch all violations in one query let violations = fetch_violations(params.conn).await?; @@ -146,8 +159,13 @@ pub async fn run_pglinter( violation.objsubid, ); + // Get rule message if available + let rule_message = rule_messages.get(&violation.rule_code); + // Create a diagnostic for this violation - if let Some(diag) = PglinterDiagnostic::from_violation(&violation.rule_code, db_object) { + if let Some(diag) = + PglinterDiagnostic::from_violation(&violation.rule_code, db_object, rule_message) + { results.push(diag); } } diff --git a/crates/pgls_schema_cache/src/indexes.rs b/crates/pgls_schema_cache/src/indexes.rs index fee160f5e..54223367a 100644 --- a/crates/pgls_schema_cache/src/indexes.rs +++ b/crates/pgls_schema_cache/src/indexes.rs @@ -1,8 +1,12 @@ +#[cfg(feature = "db")] use sqlx::PgPool; +#[cfg(feature = "db")] use crate::schema_cache::SchemaCacheItem; -#[derive(Debug, Default, PartialEq, Eq)] +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Default, PartialEq, Eq, Serialize, Deserialize)] pub struct Index { pub id: i64, pub schema: String, @@ -10,6 +14,7 @@ pub struct Index { pub table_name: String, } +#[cfg(feature = "db")] impl SchemaCacheItem for Index { type Item = Index; diff --git a/crates/pgls_schema_cache/src/sequences.rs b/crates/pgls_schema_cache/src/sequences.rs index 61146d6be..39503946e 100644 --- a/crates/pgls_schema_cache/src/sequences.rs +++ b/crates/pgls_schema_cache/src/sequences.rs @@ -1,14 +1,19 @@ +#[cfg(feature = "db")] use sqlx::PgPool; +#[cfg(feature = "db")] use crate::schema_cache::SchemaCacheItem; -#[derive(Debug, Default, PartialEq, Eq)] +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Default, PartialEq, Eq, Serialize, Deserialize)] pub struct Sequence { pub id: i64, pub schema: String, pub name: String, } +#[cfg(feature = "db")] impl SchemaCacheItem for Sequence { type Item = Sequence; From ea6ecd29415f7b1924d11e6b6cd918023286abe5 Mon Sep 17 00:00:00 2001 From: psteinroe Date: Mon, 2 Feb 2026 17:43:35 +0100 Subject: [PATCH 12/16] fix: enable db feature for pgls_schema_cache in pglinter tests --- crates/pgls_pglinter/Cargo.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/pgls_pglinter/Cargo.toml b/crates/pgls_pglinter/Cargo.toml index 983e00a8f..a1fa3eefe 100644 --- a/crates/pgls_pglinter/Cargo.toml +++ b/crates/pgls_pglinter/Cargo.toml @@ -23,6 +23,7 @@ sqlx.workspace = true [dev-dependencies] insta.workspace = true pgls_console.workspace = true +pgls_schema_cache = { workspace = true, features = ["db"] } pgls_test_utils.workspace = true [lib] From 853cb0455164261c1547a6f88a554b75e8307e34 Mon Sep 17 00:00:00 2001 From: psteinroe Date: Tue, 3 Feb 2026 16:31:46 +0100 Subject: [PATCH 13/16] fix: update pglinter test snapshots to use rule_messages --- .../tests/snapshots/fk_without_index.snap | 6 +-- .../tests/snapshots/multiple_issues.snap | 48 +++++++++---------- .../snapshots/objects_with_uppercase.snap | 24 +++++----- .../snapshots/table_without_primary_key.snap | 6 +-- 4 files changed, 42 insertions(+), 42 deletions(-) diff --git a/crates/pgls_pglinter/tests/snapshots/fk_without_index.snap b/crates/pgls_pglinter/tests/snapshots/fk_without_index.snap index ea6afe546..84826fa73 100644 --- a/crates/pgls_pglinter/tests/snapshots/fk_without_index.snap +++ b/crates/pgls_pglinter/tests/snapshots/fk_without_index.snap @@ -5,9 +5,9 @@ snapshot_kind: text --- Category: pglinter/base/howManyTableWithoutIndexOnFk Severity: Warning -Message: Foreign key on Object has no index +Message: Object does not have an index on its foreign key. Advices: -Foreign keys without indexes cause slow cascading operations and inefficient join queries. +Create an index on the foreign key column to improve join and lookup performance. [Info] Rule: B003 How to fix: -[Info] 1. create a index on foreign key or change warning/error threshold +[Info] 1. How to fix: CREATE INDEX ON {object} (...); diff --git a/crates/pgls_pglinter/tests/snapshots/multiple_issues.snap b/crates/pgls_pglinter/tests/snapshots/multiple_issues.snap index 2c803dff6..ecb249e7d 100644 --- a/crates/pgls_pglinter/tests/snapshots/multiple_issues.snap +++ b/crates/pgls_pglinter/tests/snapshots/multiple_issues.snap @@ -5,119 +5,119 @@ snapshot_kind: text --- Category: pglinter/base/howManyObjectsWithUppercase Severity: Warning -Message: Sequence 'public.BadName_id_seq' uses uppercase characters +Message: 'public.BadName_id_seq' uses uppercase characters. Advices: Using uppercase in identifiers requires quoting and can cause case-sensitivity issues. [Info] Rule: B005 How to fix: -[Info] 1. Do not use uppercase for any database objects +[Info] 1. How to fix: Rename the database object to use only lowercase characters. --- Category: pglinter/base/howManyObjectsWithUppercase Severity: Warning -Message: Ordinary 'public.BadName' uses uppercase characters +Message: 'public.BadName' uses uppercase characters. Advices: Using uppercase in identifiers requires quoting and can cause case-sensitivity issues. [Info] Rule: B005 How to fix: -[Info] 1. Do not use uppercase for any database objects +[Info] 1. How to fix: Rename the database object to use only lowercase characters. --- Category: pglinter/base/howManyObjectsWithUppercase Severity: Warning -Message: Index 'public.BadName_pkey' uses uppercase characters +Message: 'public.BadName_pkey' uses uppercase characters. Advices: Using uppercase in identifiers requires quoting and can cause case-sensitivity issues. [Info] Rule: B005 How to fix: -[Info] 1. Do not use uppercase for any database objects +[Info] 1. How to fix: Rename the database object to use only lowercase characters. --- Category: pglinter/base/howManyObjectsWithUppercase Severity: Warning -Message: Index 'public.BadName_pkey' uses uppercase characters +Message: 'public.BadName_pkey' uses uppercase characters. Advices: Using uppercase in identifiers requires quoting and can cause case-sensitivity issues. [Info] Rule: B005 How to fix: -[Info] 1. Do not use uppercase for any database objects +[Info] 1. How to fix: Rename the database object to use only lowercase characters. --- Category: pglinter/base/howManyObjectsWithUppercase Severity: Warning -Message: Sequence 'public.BadName_id_seq' uses uppercase characters +Message: 'public.BadName_id_seq' uses uppercase characters. Advices: Using uppercase in identifiers requires quoting and can cause case-sensitivity issues. [Info] Rule: B005 How to fix: -[Info] 1. Do not use uppercase for any database objects +[Info] 1. How to fix: Rename the database object to use only lowercase characters. --- Category: pglinter/base/howManyObjectsWithUppercase Severity: Warning -Message: Object Object uses uppercase characters +Message: Object uses uppercase characters. Advices: Using uppercase in identifiers requires quoting and can cause case-sensitivity issues. [Info] Rule: B005 How to fix: -[Info] 1. Do not use uppercase for any database objects +[Info] 1. How to fix: Rename the database object to use only lowercase characters. --- Category: pglinter/base/howManyObjectsWithUppercase Severity: Warning -Message: Object Object uses uppercase characters +Message: Object uses uppercase characters. Advices: Using uppercase in identifiers requires quoting and can cause case-sensitivity issues. [Info] Rule: B005 How to fix: -[Info] 1. Do not use uppercase for any database objects +[Info] 1. How to fix: Rename the database object to use only lowercase characters. --- Category: pglinter/base/howManyObjectsWithUppercase Severity: Warning -Message: Object Object uses uppercase characters +Message: Object uses uppercase characters. Advices: Using uppercase in identifiers requires quoting and can cause case-sensitivity issues. [Info] Rule: B005 How to fix: -[Info] 1. Do not use uppercase for any database objects +[Info] 1. How to fix: Rename the database object to use only lowercase characters. --- Category: pglinter/base/howManyObjectsWithUppercase Severity: Warning -Message: Object Object uses uppercase characters +Message: Object uses uppercase characters. Advices: Using uppercase in identifiers requires quoting and can cause case-sensitivity issues. [Info] Rule: B005 How to fix: -[Info] 1. Do not use uppercase for any database objects +[Info] 1. How to fix: Rename the database object to use only lowercase characters. --- Category: pglinter/base/howManyTableWithoutIndexOnFk Severity: Warning -Message: Foreign key on Object has no index +Message: Object does not have an index on its foreign key. Advices: -Foreign keys without indexes cause slow cascading operations and inefficient join queries. +Create an index on the foreign key column to improve join and lookup performance. [Info] Rule: B003 How to fix: -[Info] 1. create a index on foreign key or change warning/error threshold +[Info] 1. How to fix: CREATE INDEX ON {object} (...); --- Category: pglinter/base/howManyTableWithoutPrimaryKey Severity: Warning -Message: Table 'public.no_pk' has no primary key +Message: 'public.no_pk' does not have a primary key. Advices: -Tables without primary keys cannot be uniquely identified, which causes issues with replication, foreign keys, and efficient updates/deletes. +Add a primary key to this table to ensure data integrity and better performance. [Info] Rule: B001 How to fix: -[Info] 1. create a primary key or change warning/error threshold +[Info] 1. How to fix: ALTER TABLE {object} ADD PRIMARY KEY (...); diff --git a/crates/pgls_pglinter/tests/snapshots/objects_with_uppercase.snap b/crates/pgls_pglinter/tests/snapshots/objects_with_uppercase.snap index 68d4d88cd..9bd90774c 100644 --- a/crates/pgls_pglinter/tests/snapshots/objects_with_uppercase.snap +++ b/crates/pgls_pglinter/tests/snapshots/objects_with_uppercase.snap @@ -5,64 +5,64 @@ snapshot_kind: text --- Category: pglinter/base/howManyObjectsWithUppercase Severity: Warning -Message: Sequence 'public.TestTable_id_seq' uses uppercase characters +Message: 'public.TestTable_id_seq' uses uppercase characters. Advices: Using uppercase in identifiers requires quoting and can cause case-sensitivity issues. [Info] Rule: B005 How to fix: -[Info] 1. Do not use uppercase for any database objects +[Info] 1. How to fix: Rename the database object to use only lowercase characters. --- Category: pglinter/base/howManyObjectsWithUppercase Severity: Warning -Message: Ordinary 'public.TestTable' uses uppercase characters +Message: 'public.TestTable' uses uppercase characters. Advices: Using uppercase in identifiers requires quoting and can cause case-sensitivity issues. [Info] Rule: B005 How to fix: -[Info] 1. Do not use uppercase for any database objects +[Info] 1. How to fix: Rename the database object to use only lowercase characters. --- Category: pglinter/base/howManyObjectsWithUppercase Severity: Warning -Message: Index 'public.TestTable_pkey' uses uppercase characters +Message: 'public.TestTable_pkey' uses uppercase characters. Advices: Using uppercase in identifiers requires quoting and can cause case-sensitivity issues. [Info] Rule: B005 How to fix: -[Info] 1. Do not use uppercase for any database objects +[Info] 1. How to fix: Rename the database object to use only lowercase characters. --- Category: pglinter/base/howManyObjectsWithUppercase Severity: Warning -Message: Column 'public.TestTable.UserName' uses uppercase characters +Message: 'public.TestTable.UserName' uses uppercase characters. Advices: Using uppercase in identifiers requires quoting and can cause case-sensitivity issues. [Info] Rule: B005 How to fix: -[Info] 1. Do not use uppercase for any database objects +[Info] 1. How to fix: Rename the database object to use only lowercase characters. --- Category: pglinter/base/howManyObjectsWithUppercase Severity: Warning -Message: Index 'public.TestTable_pkey' uses uppercase characters +Message: 'public.TestTable_pkey' uses uppercase characters. Advices: Using uppercase in identifiers requires quoting and can cause case-sensitivity issues. [Info] Rule: B005 How to fix: -[Info] 1. Do not use uppercase for any database objects +[Info] 1. How to fix: Rename the database object to use only lowercase characters. --- Category: pglinter/base/howManyObjectsWithUppercase Severity: Warning -Message: Sequence 'public.TestTable_id_seq' uses uppercase characters +Message: 'public.TestTable_id_seq' uses uppercase characters. Advices: Using uppercase in identifiers requires quoting and can cause case-sensitivity issues. [Info] Rule: B005 How to fix: -[Info] 1. Do not use uppercase for any database objects +[Info] 1. How to fix: Rename the database object to use only lowercase characters. diff --git a/crates/pgls_pglinter/tests/snapshots/table_without_primary_key.snap b/crates/pgls_pglinter/tests/snapshots/table_without_primary_key.snap index 5de8cc4e0..32de9b211 100644 --- a/crates/pgls_pglinter/tests/snapshots/table_without_primary_key.snap +++ b/crates/pgls_pglinter/tests/snapshots/table_without_primary_key.snap @@ -5,9 +5,9 @@ snapshot_kind: text --- Category: pglinter/base/howManyTableWithoutPrimaryKey Severity: Warning -Message: Table 'public.test_no_pk' has no primary key +Message: 'public.test_no_pk' does not have a primary key. Advices: -Tables without primary keys cannot be uniquely identified, which causes issues with replication, foreign keys, and efficient updates/deletes. +Add a primary key to this table to ensure data integrity and better performance. [Info] Rule: B001 How to fix: -[Info] 1. create a primary key or change warning/error threshold +[Info] 1. How to fix: ALTER TABLE {object} ADD PRIMARY KEY (...); From 479065986897665b71244f5f6767a28f2492da3e Mon Sep 17 00:00:00 2001 From: psteinroe Date: Tue, 3 Feb 2026 17:50:34 +0100 Subject: [PATCH 14/16] chore: update Cargo.lock Update lockfile to resolve windows-sys and json-strip-comments dependencies. Co-Authored-By: Claude Opus 4.5 --- Cargo.lock | 28 +++++++++++++++++++--------- 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 659e8c82f..6ddd4850a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -87,7 +87,7 @@ version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -98,7 +98,7 @@ checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -1359,7 +1359,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -2039,7 +2039,7 @@ checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46" dependencies = [ "hermit-abi 0.5.2", "libc", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -2106,6 +2106,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "json-strip-comments" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25376d12b2f6ae53f986f86e2a808a56af03d72284ae24fc35a2e290d09ee3c3" +dependencies = [ + "memchr", +] + [[package]] name = "kv-log-macro" version = "1.0.7" @@ -2444,7 +2453,7 @@ version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -2557,7 +2566,7 @@ dependencies = [ "dashmap 6.1.0", "dunce", "indexmap 2.13.0", - "json-strip-comments", + "json-strip-comments 1.0.4", "once_cell", "rustc-hash 2.1.1", "serde", @@ -3228,6 +3237,7 @@ dependencies = [ "futures", "globset", "ignore", + "json-strip-comments 3.1.0", "lru", "pgls_analyse", "pgls_analyser", @@ -3909,7 +3919,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.11.0", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -4566,7 +4576,7 @@ dependencies = [ "getrandom 0.3.4", "once_cell", "rustix 1.1.3", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -5426,7 +5436,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.48.0", + "windows-sys 0.61.2", ] [[package]] From a373c3a529c22749d7195c9d009a4331912f0e74 Mon Sep 17 00:00:00 2001 From: psteinroe Date: Tue, 3 Feb 2026 17:55:52 +0100 Subject: [PATCH 15/16] fix(schema_cache): add serde default for backward compatible deserialization Allow partial JSON schemas to be deserialized by defaulting missing fields to empty vectors. This fixes WASM tests that don't provide all schema fields. Co-Authored-By: Claude Opus 4.5 --- crates/pgls_schema_cache/src/schema_cache.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/pgls_schema_cache/src/schema_cache.rs b/crates/pgls_schema_cache/src/schema_cache.rs index bf8f987c3..5fc06500a 100644 --- a/crates/pgls_schema_cache/src/schema_cache.rs +++ b/crates/pgls_schema_cache/src/schema_cache.rs @@ -14,6 +14,7 @@ use crate::versions::Version; use crate::{Extension, Role, Trigger}; #[derive(Debug, Default, Serialize, Deserialize)] +#[serde(default)] pub struct SchemaCache { pub schemas: Vec, pub tables: Vec, From e59a28ed3e5461abbfaae532ba84cd7cfd18001d Mon Sep 17 00:00:00 2001 From: psteinroe Date: Thu, 5 Feb 2026 09:15:08 +0100 Subject: [PATCH 16/16] chore: update pglinter to use main branch Switch from the development branch to main which now includes the get_violations API and rule_messages table (v1.1.0+). Co-Authored-By: Claude Opus 4.5 --- .github/actions/setup-postgres/action.yml | 4 ++-- Dockerfile | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/actions/setup-postgres/action.yml b/.github/actions/setup-postgres/action.yml index 4289f1d2a..492a0f50e 100644 --- a/.github/actions/setup-postgres/action.yml +++ b/.github/actions/setup-postgres/action.yml @@ -78,9 +78,9 @@ runs: # Initialize pgrx for the installed PostgreSQL version cargo pgrx init --pg${PG_VERSION} $(which pg_config) - # Clone and build pglinter (clone to /tmp, use feat/83/violation_list for rule_messages) + # Clone and build pglinter (requires v1.1.0+ for get_violations API + rule_messages table) cd /tmp - git clone -b feat/83/violation_list https://github.com/pmpetit/pglinter.git + git clone --depth 1 https://github.com/pmpetit/pglinter.git cd pglinter # Install using pgrx diff --git a/Dockerfile b/Dockerfile index 94b1a1e0c..a67614310 100644 --- a/Dockerfile +++ b/Dockerfile @@ -19,9 +19,9 @@ RUN apt-get update && \ cargo install cargo-pgrx --version 0.16.1 --locked && \ # Initialize pgrx for PostgreSQL 15 cargo pgrx init --pg15 $(which pg_config) && \ - # Clone and build pglinter (using feat/83/violation_list branch for get_violations API + rule_messages) + # Clone and build pglinter (requires v1.1.0+ for get_violations API + rule_messages table) cd /tmp && \ - git clone -b feat/83/violation_list https://github.com/pmpetit/pglinter.git && \ + git clone --depth 1 https://github.com/pmpetit/pglinter.git && \ cd pglinter && \ cargo pgrx install --pg-config $(which pg_config) --release && \ # Cleanup Rust and build dependencies