Skip to content

Commit 59d1c1d

Browse files
committed
chore(splinter): initial commit
1 parent 9952744 commit 59d1c1d

25 files changed

+2211
-2
lines changed

.github/workflows/pull_request.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -272,6 +272,8 @@ jobs:
272272
run: cargo run -p xtask_codegen -- configuration
273273
- name: Run the bindings codegen
274274
run: cargo run -p xtask_codegen -- bindings
275+
- name: Run the splinter codegen
276+
run: cargo run -p xtask_codegen -- splinter
275277
- name: Run the docs codegen
276278
run: cargo run -p docs_codegen
277279
- name: Check for git diff -- run "just ready" if you see an error

Cargo.lock

Lines changed: 14 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ pgls_query = { path = "./crates/pgls_query", version = "0.0.0"
8282
pgls_query_ext = { path = "./crates/pgls_query_ext", version = "0.0.0" }
8383
pgls_query_macros = { path = "./crates/pgls_query_macros", version = "0.0.0" }
8484
pgls_schema_cache = { path = "./crates/pgls_schema_cache", version = "0.0.0" }
85+
pgls_splinter = { path = "./crates/pgls_splinter", version = "0.0.0" }
8586
pgls_statement_splitter = { path = "./crates/pgls_statement_splitter", version = "0.0.0" }
8687
pgls_suppressions = { path = "./crates/pgls_suppressions", version = "0.0.0" }
8788
pgls_text_edit = { path = "./crates/pgls_text_edit", version = "0.0.0" }

crates/pgls_diagnostics_categories/src/categories.rs

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,30 @@ define_categories! {
4747
"lint/safety/runningStatementWhileHoldingAccessExclusive": "https://pg-language-server.com/latest/rules/running-statement-while-holding-access-exclusive",
4848
"lint/safety/transactionNesting": "https://pg-language-server.com/latest/rules/transaction-nesting",
4949
// end lint rules
50+
// splinter rules start
51+
"dblint/splinter/authRlsInitplan": "https://supabase.com/docs/guides/database/database-linter?lint=0003_auth_rls_initplan",
52+
"dblint/splinter/authUsersExposed": "https://supabase.com/docs/guides/database/database-linter?lint=0002_auth_users_exposed",
53+
"dblint/splinter/duplicateIndex": "https://supabase.com/docs/guides/database/database-linter?lint=0009_duplicate_index",
54+
"dblint/splinter/extensionInPublic": "https://supabase.com/docs/guides/database/database-linter?lint=0014_extension_in_public",
55+
"dblint/splinter/extensionVersionsOutdated": "https://supabase.com/docs/guides/database/database-linter?lint=0022_extension_versions_outdated",
56+
"dblint/splinter/fkeyToAuthUnique": "https://supabase.com/docs/guides/database/database-linter?lint=0021_fkey_to_auth_unique",
57+
"dblint/splinter/foreignTableInApi": "https://supabase.com/docs/guides/database/database-linter?lint=0017_foreign_table_in_api",
58+
"dblint/splinter/functionSearchPathMutable": "https://supabase.com/docs/guides/database/database-linter?lint=0011_function_search_path_mutable",
59+
"dblint/splinter/insecureQueueExposedInApi": "https://supabase.com/docs/guides/database/database-linter?lint=0019_insecure_queue_exposed_in_api",
60+
"dblint/splinter/materializedViewInApi": "https://supabase.com/docs/guides/database/database-linter?lint=0016_materialized_view_in_api",
61+
"dblint/splinter/multiplePermissivePolicies": "https://supabase.com/docs/guides/database/database-linter?lint=0006_multiple_permissive_policies",
62+
"dblint/splinter/noPrimaryKey": "https://supabase.com/docs/guides/database/database-linter?lint=0004_no_primary_key",
63+
"dblint/splinter/policyExistsRlsDisabled": "https://supabase.com/docs/guides/database/database-linter?lint=0007_policy_exists_rls_disabled",
64+
"dblint/splinter/rlsDisabledInPublic": "https://supabase.com/docs/guides/database/database-linter?lint=0013_rls_disabled_in_public",
65+
"dblint/splinter/rlsEnabledNoPolicy": "https://supabase.com/docs/guides/database/database-linter?lint=0008_rls_enabled_no_policy",
66+
"dblint/splinter/rlsReferencesUserMetadata": "https://supabase.com/docs/guides/database/database-linter?lint=0015_rls_references_user_metadata",
67+
"dblint/splinter/securityDefinerView": "https://supabase.com/docs/guides/database/database-linter?lint=0010_security_definer_view",
68+
"dblint/splinter/tableBloat": "https://supabase.com/docs/guides/database/database-linter?lint=0020_table_bloat",
69+
"dblint/splinter/unindexedForeignKeys": "https://supabase.com/docs/guides/database/database-linter?lint=0001_unindexed_foreign_keys",
70+
"dblint/splinter/unknown": "https://pg-language-server.com/latest",
71+
"dblint/splinter/unsupportedRegTypes": "https://supabase.com/docs/guides/database/database-linter?lint=unsupported_reg_types",
72+
"dblint/splinter/unusedIndex": "https://supabase.com/docs/guides/database/database-linter?lint=0005_unused_index",
73+
// splinter rules end
5074
;
5175
// General categories
5276
"stdin",
@@ -68,5 +92,9 @@ define_categories! {
6892
"lint",
6993
"lint/performance",
7094
"lint/safety",
95+
// splinter groups start
96+
"dblint",
97+
"dblint/splinter",
98+
// splinter groups end
7199
// Lint groups end
72-
}
100+
}

crates/pgls_splinter/.sqlx/query-b869d517301aaf69d382f092c09d5a53712d68afd273423f9310cd793586f532.json

Lines changed: 74 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/pgls_splinter/Cargo.toml

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
[package]
2+
authors.workspace = true
3+
categories.workspace = true
4+
description = "<DESCRIPTION>"
5+
edition.workspace = true
6+
homepage.workspace = true
7+
keywords.workspace = true
8+
license.workspace = true
9+
name = "pgls_splinter"
10+
repository.workspace = true
11+
version = "0.0.0"
12+
13+
[dependencies]
14+
pgls_diagnostics.workspace = true
15+
serde.workspace = true
16+
serde_json.workspace = true
17+
sqlx.workspace = true
18+
19+
[build-dependencies]
20+
ureq = "2.10"
21+
22+
[dev-dependencies]
23+
insta.workspace = true
24+
pgls_console.workspace = true
25+
pgls_test_utils.workspace = true
26+
27+
[lib]
28+
doctest = false

crates/pgls_splinter/TODO.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
- implement ignore / include and config. try to refactor existing analyser infrastructure to be re-used.

crates/pgls_splinter/build.rs

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
use std::env;
2+
use std::fs;
3+
use std::path::Path;
4+
5+
// Update this commit SHA to pull in a new version of splinter.sql
6+
const SPLINTER_COMMIT_SHA: &str = "27ea2ece65464213e466cd969cc61b6940d16219";
7+
8+
fn main() {
9+
let out_dir = env::var("CARGO_MANIFEST_DIR").unwrap();
10+
let vendor_dir = Path::new(&out_dir).join("vendor");
11+
let sql_file = vendor_dir.join("splinter.sql");
12+
let sha_file = vendor_dir.join("COMMIT_SHA.txt");
13+
14+
// Create vendor directory if it doesn't exist
15+
if !vendor_dir.exists() {
16+
fs::create_dir_all(&vendor_dir).expect("Failed to create vendor directory");
17+
}
18+
19+
// Check if we need to download
20+
let needs_download = if !sql_file.exists() || !sha_file.exists() {
21+
true
22+
} else {
23+
// Check if stored SHA matches current constant
24+
let stored_sha = fs::read_to_string(&sha_file)
25+
.expect("Failed to read COMMIT_SHA.txt")
26+
.trim()
27+
.to_string();
28+
stored_sha != SPLINTER_COMMIT_SHA
29+
};
30+
31+
if needs_download {
32+
println!(
33+
"cargo:warning=Downloading splinter.sql from GitHub (commit: {SPLINTER_COMMIT_SHA})"
34+
);
35+
download_and_process_sql(&sql_file);
36+
fs::write(&sha_file, SPLINTER_COMMIT_SHA).expect("Failed to write COMMIT_SHA.txt");
37+
}
38+
39+
// Tell cargo to rerun if build.rs or SHA file changes
40+
println!("cargo:rerun-if-changed=build.rs");
41+
println!("cargo:rerun-if-changed=vendor/COMMIT_SHA.txt");
42+
}
43+
44+
fn download_and_process_sql(dest_path: &Path) {
45+
let url = format!(
46+
"https://raw.githubusercontent.com/supabase/splinter/{SPLINTER_COMMIT_SHA}/splinter.sql"
47+
);
48+
49+
// Download the file
50+
let response = ureq::get(&url)
51+
.call()
52+
.expect("Failed to download splinter.sql");
53+
54+
let content = response
55+
.into_string()
56+
.expect("Failed to read response body");
57+
58+
// Remove the SET LOCAL search_path section
59+
let mut processed_content = remove_set_search_path(&content);
60+
61+
// Add "!" suffix to column aliases for sqlx non-null checking
62+
processed_content = add_not_null_markers(&processed_content);
63+
64+
// Write to destination
65+
fs::write(dest_path, processed_content).expect("Failed to write splinter.sql");
66+
67+
println!("cargo:warning=Successfully downloaded and processed splinter.sql");
68+
}
69+
70+
fn remove_set_search_path(content: &str) -> String {
71+
content
72+
.lines()
73+
.filter(|line| {
74+
let trimmed = line.trim();
75+
!trimmed.to_lowercase().starts_with("set local search_path")
76+
})
77+
.collect::<Vec<_>>()
78+
.join("\n")
79+
}
80+
81+
fn add_not_null_markers(content: &str) -> String {
82+
// Add "!" suffix to all column aliases to mark them as non-null for sqlx
83+
// This transforms patterns like: 'value' as name
84+
// Into: 'value' as "name!"
85+
86+
let columns_to_mark = [
87+
"name",
88+
"title",
89+
"level",
90+
"facing",
91+
"categories",
92+
"description",
93+
"detail",
94+
"remediation",
95+
"metadata",
96+
"cache_key",
97+
];
98+
99+
let mut result = content.to_string();
100+
101+
for column in &columns_to_mark {
102+
// Match patterns like: as name, as name)
103+
let pattern_comma = format!(" as {column}");
104+
let replacement_comma = format!(" as \"{column}!\"");
105+
result = result.replace(&pattern_comma, &replacement_comma);
106+
}
107+
108+
result
109+
}
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
use pgls_diagnostics::{Category, Severity, category};
2+
use serde_json::Value;
3+
4+
use crate::{SplinterAdvices, SplinterDiagnostic, SplinterQueryResult};
5+
6+
impl From<SplinterQueryResult> for SplinterDiagnostic {
7+
fn from(result: SplinterQueryResult) -> Self {
8+
let severity = parse_severity(&result.level);
9+
10+
// Extract common fields from metadata
11+
let (schema, object_name, object_type, additional_metadata) =
12+
extract_metadata_fields(&result.metadata);
13+
14+
SplinterDiagnostic {
15+
category: rule_name_to_category(&result.name),
16+
message: result.detail.into(),
17+
severity,
18+
advices: SplinterAdvices {
19+
description: result.description,
20+
schema,
21+
object_name,
22+
object_type,
23+
remediation_url: result.remediation,
24+
additional_metadata,
25+
},
26+
}
27+
}
28+
}
29+
30+
/// Parse severity level from the query result
31+
fn parse_severity(level: &str) -> Severity {
32+
match level {
33+
"INFO" => Severity::Information,
34+
"WARN" => Severity::Warning,
35+
"ERROR" => Severity::Error,
36+
_ => Severity::Information, // default to info
37+
}
38+
}
39+
40+
/// Convert rule name to a Category
41+
/// Note: Rule names use snake_case, but categories use camelCase
42+
fn rule_name_to_category(name: &str) -> &'static Category {
43+
match name {
44+
"unindexed_foreign_keys" => category!("dblint/splinter/unindexedForeignKeys"),
45+
"auth_users_exposed" => category!("dblint/splinter/authUsersExposed"),
46+
"auth_rls_initplan" => category!("dblint/splinter/authRlsInitplan"),
47+
"no_primary_key" => category!("dblint/splinter/noPrimaryKey"),
48+
"unused_index" => category!("dblint/splinter/unusedIndex"),
49+
"multiple_permissive_policies" => category!("dblint/splinter/multiplePermissivePolicies"),
50+
"policy_exists_rls_disabled" => category!("dblint/splinter/policyExistsRlsDisabled"),
51+
"rls_enabled_no_policy" => category!("dblint/splinter/rlsEnabledNoPolicy"),
52+
"duplicate_index" => category!("dblint/splinter/duplicateIndex"),
53+
"security_definer_view" => category!("dblint/splinter/securityDefinerView"),
54+
"function_search_path_mutable" => category!("dblint/splinter/functionSearchPathMutable"),
55+
"rls_disabled_in_public" => category!("dblint/splinter/rlsDisabledInPublic"),
56+
"extension_in_public" => category!("dblint/splinter/extensionInPublic"),
57+
"rls_references_user_metadata" => category!("dblint/splinter/rlsReferencesUserMetadata"),
58+
"materialized_view_in_api" => category!("dblint/splinter/materializedViewInApi"),
59+
"foreign_table_in_api" => category!("dblint/splinter/foreignTableInApi"),
60+
"unsupported_reg_types" => category!("dblint/splinter/unsupportedRegTypes"),
61+
"insecure_queue_exposed_in_api" => category!("dblint/splinter/insecureQueueExposedInApi"),
62+
"table_bloat" => category!("dblint/splinter/tableBloat"),
63+
"fkey_to_auth_unique" => category!("dblint/splinter/fkeyToAuthUnique"),
64+
"extension_versions_outdated" => category!("dblint/splinter/extensionVersionsOutdated"),
65+
_ => category!("dblint/splinter/unknown"),
66+
}
67+
}
68+
69+
/// Extract common metadata fields and return the rest as additional_metadata
70+
fn extract_metadata_fields(
71+
metadata: &Value,
72+
) -> (
73+
Option<String>,
74+
Option<String>,
75+
Option<String>,
76+
Option<Value>,
77+
) {
78+
if let Some(obj) = metadata.as_object() {
79+
let schema = obj.get("schema").and_then(|v| v.as_str()).map(String::from);
80+
81+
let object_name = obj.get("name").and_then(|v| v.as_str()).map(String::from);
82+
83+
let object_type = obj.get("type").and_then(|v| v.as_str()).map(String::from);
84+
85+
// Create a new object without the common fields
86+
let mut additional = obj.clone();
87+
additional.remove("schema");
88+
additional.remove("name");
89+
additional.remove("type");
90+
91+
let additional_metadata = if additional.is_empty() {
92+
None
93+
} else {
94+
Some(Value::Object(additional))
95+
};
96+
97+
(schema, object_name, object_type, additional_metadata)
98+
} else {
99+
(None, None, None, Some(metadata.clone()))
100+
}
101+
}

0 commit comments

Comments
 (0)