Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 4 additions & 2 deletions crates/pgls_cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,10 @@ mimalloc = "0.1.43"
tikv-jemallocator = "0.6.0"

[dev-dependencies]
assert_cmd = "2.0.16"
insta = { workspace = true, features = ["yaml"] }
assert_cmd = "2.0.16"
insta = { workspace = true, features = ["yaml"] }
pgls_test_utils = { workspace = true }
sqlx = { workspace = true }

[lib]
doctest = false
Expand Down
10 changes: 9 additions & 1 deletion crates/pgls_cli/src/commands/dblint.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use std::time::Instant;
use crate::cli_options::CliOptions;
use crate::reporter::Report;
use crate::{CliDiagnostic, CliSession, VcsIntegration};
use pgls_analyse::RuleCategoriesBuilder;
use pgls_configuration::PartialConfiguration;
use pgls_diagnostics::Error;
use pgls_workspace::features::diagnostics::{PullDatabaseDiagnosticsParams, PullDiagnosticsResult};
Expand All @@ -24,10 +25,17 @@ pub fn dblint(

let start = Instant::now();

let params = PullDatabaseDiagnosticsParams {
categories: RuleCategoriesBuilder::default().all().build(),
max_diagnostics,
only: Vec::new(), // Uses configuration settings
skip: Vec::new(), // Uses configuration settings
};

let PullDiagnosticsResult {
diagnostics,
skipped_diagnostics,
} = workspace.pull_db_diagnostics(PullDatabaseDiagnosticsParams { max_diagnostics })?;
} = workspace.pull_db_diagnostics(params)?;

let report = Report::new(
diagnostics.into_iter().map(Error::from).collect(),
Expand Down
2 changes: 1 addition & 1 deletion crates/pgls_cli/src/commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ pub enum PgLSCommand {
#[bpaf(command)]
Version(#[bpaf(external(cli_options), hide_usage)] CliOptions),

/// Runs everything to the requested files.
/// Lints your database schema.
#[bpaf(command)]
Dblint {
#[bpaf(external(partial_configuration), hide_usage, optional)]
Expand Down
117 changes: 117 additions & 0 deletions crates/pgls_cli/tests/assert_dblint.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
use assert_cmd::Command;
use insta::assert_snapshot;
use sqlx::PgPool;
use std::process::ExitStatus;

const BIN: &str = "postgres-language-server";

/// Get database URL from the pool's connect options
/// Uses the known docker-compose credentials (postgres:postgres)
fn get_database_url(pool: &PgPool) -> String {
let opts = pool.connect_options();
format!(
"postgres://postgres:postgres@{}:{}/{}",
opts.get_host(),
opts.get_port(),
opts.get_database().unwrap_or("postgres")
)
}

#[sqlx::test(migrator = "pgls_test_utils::MIGRATIONS")]
#[cfg_attr(
target_os = "windows",
ignore = "snapshot expectations only validated on unix-like platforms"
)]
async fn dblint_empty_database_snapshot(test_db: PgPool) {
let url = get_database_url(&test_db);
let output = run_dblint(&url, &[]);
assert_snapshot!(output);
}

#[sqlx::test(migrator = "pgls_test_utils::MIGRATIONS")]
#[cfg_attr(
target_os = "windows",
ignore = "snapshot expectations only validated on unix-like platforms"
)]
async fn dblint_detects_issues_snapshot(test_db: PgPool) {
// Setup: create table without primary key (triggers noPrimaryKey rule)
sqlx::raw_sql("CREATE TABLE test_no_pk (id int, name text)")
.execute(&test_db)
.await
.expect("Failed to create test table");

let url = get_database_url(&test_db);
let output = run_dblint(&url, &[]);
assert_snapshot!(output);
}

#[test]
#[cfg_attr(
target_os = "windows",
ignore = "snapshot expectations only validated on unix-like platforms"
)]
fn dblint_no_database_snapshot() {
// Test that dblint completes gracefully when no database is configured
let mut cmd = Command::cargo_bin(BIN).expect("binary not built");
let output = cmd
.args(["dblint", "--disable-db", "--log-level", "none"])
.output()
.expect("failed to run CLI");

let normalized = normalize_output(
output.status,
&String::from_utf8_lossy(&output.stdout),
&String::from_utf8_lossy(&output.stderr),
);
assert_snapshot!(normalized);
}

fn run_dblint(url: &str, args: &[&str]) -> String {
let mut cmd = Command::cargo_bin(BIN).expect("binary not built");
let mut full_args = vec!["dblint", "--connection-string", url, "--log-level", "none"];
full_args.extend_from_slice(args);

let output = cmd.args(full_args).output().expect("failed to run CLI");

normalize_output(
output.status,
&String::from_utf8_lossy(&output.stdout),
&String::from_utf8_lossy(&output.stderr),
)
}

fn normalize_output(status: ExitStatus, stdout: &str, stderr: &str) -> String {
let normalized_stdout = normalize_durations(stdout);
let status_label = if status.success() {
"success"
} else {
"failure"
};
format!(
"status: {status_label}\nstdout:\n{}\nstderr:\n{}\n",
normalized_stdout.trim_end(),
stderr.trim_end()
)
}

fn normalize_durations(input: &str) -> String {
let mut content = input.to_owned();

let mut search_start = 0;
while let Some(relative) = content[search_start..].find(" in ") {
let start = search_start + relative + 4;
if let Some(end_rel) = content[start..].find('.') {
let end = start + end_rel;
if content[start..end].chars().any(|c| c.is_ascii_digit()) {
content.replace_range(start..end, "<duration>");
search_start = start + "<duration>".len() + 1;
continue;
}
search_start = end + 1;
} else {
break;
}
}

content
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
---
source: crates/pgls_cli/tests/assert_dblint.rs
expression: output
snapshot_kind: text
---
status: success
stdout:
Warning: Deprecated config filename detected. Use 'postgres-language-server.jsonc'.

Command completed in <duration>.
stderr:
splinter/performance/noPrimaryKey ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

i Table \`public.test_no_pk\` does not have a primary key

Detects if a table does not have a primary key. Tables without a primary key can be inefficient to interact with at scale.

i table: public.test_no_pk

i Remediation: https://supabase.com/docs/guides/database/database-linter?lint=0004_no_primary_key
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
source: crates/pgls_cli/tests/assert_dblint.rs
expression: output
snapshot_kind: text
---
status: success
stdout:
Warning: Deprecated config filename detected. Use 'postgres-language-server.jsonc'.

Command completed in <duration>.
stderr:
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
source: crates/pgls_cli/tests/assert_dblint.rs
expression: normalized
snapshot_kind: text
---
status: success
stdout:
Warning: Deprecated config filename detected. Use 'postgres-language-server.jsonc'.

Command completed in <duration>.
stderr:
11 changes: 11 additions & 0 deletions crates/pgls_configuration/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@ pub use rules::{
RuleWithFixOptions, RuleWithOptions,
};
use serde::{Deserialize, Serialize};
use splinter::{
PartialSplinterConfiguration, SplinterConfiguration, partial_splinter_configuration,
};
pub use typecheck::{
PartialTypecheckConfiguration, TypecheckConfiguration, partial_typecheck_configuration,
};
Expand Down Expand Up @@ -86,6 +89,10 @@ pub struct Configuration {
#[partial(type, bpaf(external(partial_linter_configuration), optional))]
pub linter: LinterConfiguration,

/// The configuration for splinter
#[partial(type, bpaf(external(partial_splinter_configuration), optional))]
pub splinter: SplinterConfiguration,

/// The configuration for type checking
#[partial(type, bpaf(external(partial_typecheck_configuration), optional))]
pub typecheck: TypecheckConfiguration,
Expand Down Expand Up @@ -127,6 +134,10 @@ impl PartialConfiguration {
}),
..Default::default()
}),
splinter: Some(PartialSplinterConfiguration {
enabled: Some(true),
..Default::default()
}),
typecheck: Some(PartialTypecheckConfiguration {
..Default::default()
}),
Expand Down
1 change: 1 addition & 0 deletions crates/pgls_configuration/src/linter/rules.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ impl std::str::FromStr for RuleGroup {
}
#[derive(Clone, Debug, Default, Deserialize, Eq, Merge, PartialEq, Serialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[cfg_attr(feature = "schema", schemars(rename = "LinterRules"))]
#[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."]
Expand Down
9 changes: 0 additions & 9 deletions crates/pgls_configuration/src/splinter/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
mod options;
pub use options::SplinterRuleOptions;
mod rules;
use biome_deserialize::StringSet;
use biome_deserialize_macros::{Merge, Partial};
use bpaf::Bpaf;
pub use rules::*;
Expand All @@ -20,12 +19,6 @@ pub struct SplinterConfiguration {
#[doc = r" List of rules"]
#[partial(bpaf(pure(Default::default()), optional, hide))]
pub rules: Rules,
#[doc = r" A list of Unix shell style patterns. The linter will ignore files/folders that will match these patterns."]
#[partial(bpaf(hide))]
pub ignore: StringSet,
#[doc = r" A list of Unix shell style patterns. The linter will include files/folders that will match these patterns."]
#[partial(bpaf(hide))]
pub include: StringSet,
}
impl SplinterConfiguration {
pub const fn is_disabled(&self) -> bool {
Expand All @@ -37,8 +30,6 @@ impl Default for SplinterConfiguration {
Self {
enabled: true,
rules: Default::default(),
ignore: Default::default(),
include: Default::default(),
}
}
}
Expand Down
Loading