Skip to content

Commit dae4622

Browse files
psteinroeclaude
andcommitted
refactor: expose structured metadata through splinter registry
Instead of re-parsing SQL comment metadata in docs codegen, expose structured metadata directly through the registry API. Changes: - Add SplinterRuleMetadata struct (description, remediation, requires_supabase) - Add get_rule_metadata() function to registry - Update docs codegen to use structured metadata - Remove duplicate SQL parsing logic - Update runtime to use new metadata API - Deprecate rule_requires_supabase() in favor of get_rule_metadata() This eliminates redundant parsing and provides a cleaner API. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 22376a6 commit dae4622

File tree

5 files changed

+190
-40
lines changed

5 files changed

+190
-40
lines changed

crates/pgls_splinter/src/lib.rs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -79,8 +79,12 @@ pub async fn run_splinter(
7979

8080
for rule_name in &collector.enabled_rules {
8181
// Skip Supabase-specific rules if Supabase roles don't exist
82-
if !has_supabase_roles && crate::registry::rule_requires_supabase(rule_name) {
83-
continue;
82+
if !has_supabase_roles {
83+
if let Some(metadata) = crate::registry::get_rule_metadata(rule_name) {
84+
if metadata.requires_supabase {
85+
continue;
86+
}
87+
}
8488
}
8589

8690
// Get embedded SQL content (compile-time included)

crates/pgls_splinter/src/registry.rs

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,16 @@
22
33
#![doc = r" Generated file, do not edit by hand, see `xtask/codegen`"]
44
use pgls_analyse::RegistryVisitor;
5+
#[doc = r" Metadata for a splinter rule"]
6+
#[derive(Debug, Clone, Copy)]
7+
pub struct SplinterRuleMetadata {
8+
#[doc = r" Description of what the rule detects"]
9+
pub description: &'static str,
10+
#[doc = r" URL to documentation/remediation guide"]
11+
pub remediation: &'static str,
12+
#[doc = r" Whether this rule requires Supabase roles (anon, authenticated, service_role)"]
13+
pub requires_supabase: bool,
14+
}
515
#[doc = r" Visit all splinter rules using the visitor pattern"]
616
#[doc = r" This is called during registry building to collect enabled rules"]
717
pub fn visit_registry<V: RegistryVisitor>(registry: &mut V) {
@@ -151,6 +161,120 @@ pub fn get_sql_content(rule_name: &str) -> Option<&'static str> {
151161
_ => None,
152162
}
153163
}
164+
#[doc = r" Get metadata for a rule (camelCase name)"]
165+
#[doc = r" Returns None if rule not found"]
166+
#[doc = r""]
167+
#[doc = r" This provides structured access to rule metadata without requiring SQL parsing"]
168+
pub fn get_rule_metadata(rule_name: &str) -> Option<SplinterRuleMetadata> {
169+
match rule_name {
170+
"authRlsInitplan" => Some(SplinterRuleMetadata {
171+
description: "Detects if calls to \\`current_setting()\\` and \\`auth.<function>()\\` in RLS policies are being unnecessarily re-evaluated for each row",
172+
remediation: "https://supabase.com/docs/guides/database/database-linter?lint=0003_auth_rls_initplan",
173+
requires_supabase: true,
174+
}),
175+
"authUsersExposed" => Some(SplinterRuleMetadata {
176+
description: "Detects if auth.users is exposed to anon or authenticated roles via a view or materialized view in schemas exposed to PostgREST, potentially compromising user data security.",
177+
remediation: "https://supabase.com/docs/guides/database/database-linter?lint=0002_auth_users_exposed",
178+
requires_supabase: true,
179+
}),
180+
"duplicateIndex" => Some(SplinterRuleMetadata {
181+
description: "Detects cases where two ore more identical indexes exist.",
182+
remediation: "https://supabase.com/docs/guides/database/database-linter?lint=0009_duplicate_index",
183+
requires_supabase: false,
184+
}),
185+
"extensionInPublic" => Some(SplinterRuleMetadata {
186+
description: "Detects extensions installed in the \\`public\\` schema.",
187+
remediation: "https://supabase.com/docs/guides/database/database-linter?lint=0014_extension_in_public",
188+
requires_supabase: false,
189+
}),
190+
"extensionVersionsOutdated" => Some(SplinterRuleMetadata {
191+
description: "Detects extensions that are not using the default (recommended) version.",
192+
remediation: "https://supabase.com/docs/guides/database/database-linter?lint=0022_extension_versions_outdated",
193+
requires_supabase: false,
194+
}),
195+
"fkeyToAuthUnique" => Some(SplinterRuleMetadata {
196+
description: "Detects user defined foreign keys to unique constraints in the auth schema.",
197+
remediation: "Drop the foreign key constraint that references the auth schema.",
198+
requires_supabase: true,
199+
}),
200+
"foreignTableInApi" => Some(SplinterRuleMetadata {
201+
description: "Detects foreign tables that are accessible over APIs. Foreign tables do not respect row level security policies.",
202+
remediation: "https://supabase.com/docs/guides/database/database-linter?lint=0017_foreign_table_in_api",
203+
requires_supabase: true,
204+
}),
205+
"functionSearchPathMutable" => Some(SplinterRuleMetadata {
206+
description: "Detects functions where the search_path parameter is not set.",
207+
remediation: "https://supabase.com/docs/guides/database/database-linter?lint=0011_function_search_path_mutable",
208+
requires_supabase: false,
209+
}),
210+
"insecureQueueExposedInApi" => Some(SplinterRuleMetadata {
211+
description: "Detects cases where an insecure Queue is exposed over Data APIs",
212+
remediation: "https://supabase.com/docs/guides/database/database-linter?lint=0019_insecure_queue_exposed_in_api",
213+
requires_supabase: true,
214+
}),
215+
"materializedViewInApi" => Some(SplinterRuleMetadata {
216+
description: "Detects materialized views that are accessible over the Data APIs.",
217+
remediation: "https://supabase.com/docs/guides/database/database-linter?lint=0016_materialized_view_in_api",
218+
requires_supabase: true,
219+
}),
220+
"multiplePermissivePolicies" => Some(SplinterRuleMetadata {
221+
description: "Detects if multiple permissive row level security policies are present on a table for the same \\`role\\` and \\`action\\` (e.g. insert). Multiple permissive policies are suboptimal for performance as each policy must be executed for every relevant query.",
222+
remediation: "https://supabase.com/docs/guides/database/database-linter?lint=0006_multiple_permissive_policies",
223+
requires_supabase: false,
224+
}),
225+
"noPrimaryKey" => Some(SplinterRuleMetadata {
226+
description: "Detects if a table does not have a primary key. Tables without a primary key can be inefficient to interact with at scale.",
227+
remediation: "https://supabase.com/docs/guides/database/database-linter?lint=0004_no_primary_key",
228+
requires_supabase: false,
229+
}),
230+
"policyExistsRlsDisabled" => Some(SplinterRuleMetadata {
231+
description: "Detects cases where row level security (RLS) policies have been created, but RLS has not been enabled for the underlying table.",
232+
remediation: "https://supabase.com/docs/guides/database/database-linter?lint=0007_policy_exists_rls_disabled",
233+
requires_supabase: false,
234+
}),
235+
"rlsDisabledInPublic" => Some(SplinterRuleMetadata {
236+
description: "Detects cases where row level security (RLS) has not been enabled on tables in schemas exposed to PostgREST",
237+
remediation: "https://supabase.com/docs/guides/database/database-linter?lint=0013_rls_disabled_in_public",
238+
requires_supabase: true,
239+
}),
240+
"rlsEnabledNoPolicy" => Some(SplinterRuleMetadata {
241+
description: "Detects cases where row level security (RLS) has been enabled on a table but no RLS policies have been created.",
242+
remediation: "https://supabase.com/docs/guides/database/database-linter?lint=0008_rls_enabled_no_policy",
243+
requires_supabase: false,
244+
}),
245+
"rlsReferencesUserMetadata" => Some(SplinterRuleMetadata {
246+
description: "Detects when Supabase Auth user_metadata is referenced insecurely in a row level security (RLS) policy.",
247+
remediation: "https://supabase.com/docs/guides/database/database-linter?lint=0015_rls_references_user_metadata",
248+
requires_supabase: true,
249+
}),
250+
"securityDefinerView" => Some(SplinterRuleMetadata {
251+
description: "Detects views defined with the SECURITY DEFINER property. These views enforce Postgres permissions and row level security policies (RLS) of the view creator, rather than that of the querying user",
252+
remediation: "https://supabase.com/docs/guides/database/database-linter?lint=0010_security_definer_view",
253+
requires_supabase: true,
254+
}),
255+
"tableBloat" => Some(SplinterRuleMetadata {
256+
description: "Detects if a table has excess bloat and may benefit from maintenance operations like vacuum full or cluster.",
257+
remediation: "Consider running vacuum full (WARNING: incurs downtime) and tweaking autovacuum settings to reduce bloat.",
258+
requires_supabase: false,
259+
}),
260+
"unindexedForeignKeys" => Some(SplinterRuleMetadata {
261+
description: "Identifies foreign key constraints without a covering index, which can impact database performance.",
262+
remediation: "https://supabase.com/docs/guides/database/database-linter?lint=0001_unindexed_foreign_keys",
263+
requires_supabase: false,
264+
}),
265+
"unsupportedRegTypes" => Some(SplinterRuleMetadata {
266+
description: "Identifies columns using unsupported reg* types outside pg_catalog schema, which prevents database upgrades using pg_upgrade.",
267+
remediation: "https://supabase.com/docs/guides/database/database-linter?lint=unsupported_reg_types",
268+
requires_supabase: false,
269+
}),
270+
"unusedIndex" => Some(SplinterRuleMetadata {
271+
description: "Detects if an index has never been used and may be a candidate for removal.",
272+
remediation: "https://supabase.com/docs/guides/database/database-linter?lint=0005_unused_index",
273+
requires_supabase: false,
274+
}),
275+
_ => None,
276+
}
277+
}
154278
#[doc = r" Map rule name from SQL result (snake_case) to diagnostic category"]
155279
#[doc = r" Returns None if rule not found"]
156280
#[doc = r""]
@@ -225,6 +349,7 @@ pub fn get_rule_category(rule_name: &str) -> Option<&'static ::pgls_diagnostics:
225349
}
226350
#[doc = r" Check if a rule requires Supabase roles (anon, authenticated, service_role)"]
227351
#[doc = r" Rules that require Supabase should be filtered out if these roles don't exist"]
352+
#[deprecated(note = "Use get_rule_metadata() instead")]
228353
pub fn rule_requires_supabase(rule_name: &str) -> bool {
229354
match rule_name {
230355
"authRlsInitplan" => true,

docs/codegen/src/splinter_docs.rs

Lines changed: 7 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,6 @@ use std::{fs, io::Write as _, path::Path};
44

55
use crate::utils::SplinterRuleMetadata;
66

7-
/// Extract remediation URL from SQL metadata comments
8-
fn extract_remediation_from_sql(sql: &str) -> Option<String> {
9-
for line in sql.lines() {
10-
if let Some(url) = line.strip_prefix("-- meta: remediation = ") {
11-
return Some(url.trim().to_string());
12-
}
13-
}
14-
None
15-
}
16-
177
/// Strip metadata comments from SQL content
188
/// Removes all lines starting with "-- meta:"
199
fn strip_metadata_from_sql(sql: &str) -> String {
@@ -80,7 +70,7 @@ fn generate_splinter_rule_doc(
8070
writeln!(content)?;
8171

8272
// Add Supabase requirement notice
83-
if splinter_meta.requires_supabase {
73+
if splinter_meta.registry_metadata.requires_supabase {
8474
writeln!(content, "> [!NOTE]")?;
8575
writeln!(
8676
content,
@@ -92,15 +82,14 @@ fn generate_splinter_rule_doc(
9282
writeln!(content, "## Description")?;
9383
writeln!(content)?;
9484

95-
// Use description from SQL metadata
96-
writeln!(content, "{}", splinter_meta.description)?;
85+
// Use description from registry metadata
86+
writeln!(content, "{}", splinter_meta.registry_metadata.description)?;
9787
writeln!(content)?;
9888

99-
// Add "Learn More" link with remediation URL
100-
if let Some(remediation) = extract_remediation_from_sql(splinter_meta.sql_content) {
101-
writeln!(content, "[Learn More]({remediation})")?;
102-
writeln!(content)?;
103-
}
89+
// Add "Learn More" link with remediation URL from registry metadata
90+
let remediation = splinter_meta.registry_metadata.remediation;
91+
writeln!(content, "[Learn More]({remediation})")?;
92+
writeln!(content)?;
10493

10594
// Add SQL query section (with metadata stripped)
10695
writeln!(content, "## SQL Query")?;

docs/codegen/src/utils.rs

Lines changed: 10 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,12 @@ use pgls_analyse::{
44
use regex::Regex;
55
use std::collections::BTreeMap;
66

7-
/// Metadata for a splinter rule with SQL content
7+
/// Metadata for a splinter rule with SQL content and registry metadata
88
#[derive(Clone)]
99
pub(crate) struct SplinterRuleMetadata {
1010
pub(crate) metadata: RuleMetadata,
1111
pub(crate) sql_content: &'static str,
12-
pub(crate) requires_supabase: bool,
13-
pub(crate) description: String,
12+
pub(crate) registry_metadata: pgls_splinter::registry::SplinterRuleMetadata,
1413
}
1514

1615
pub(crate) fn replace_section(
@@ -95,32 +94,23 @@ impl RegistryVisitor for SplinterRulesVisitor {
9594
.entry(<R::Group as RuleGroup>::NAME)
9695
.or_default();
9796

98-
// Get SQL content and Supabase requirement from registry
97+
// Get SQL content and metadata from registry
9998
let sql_content = pgls_splinter::registry::get_sql_content(R::METADATA.name)
10099
.unwrap_or("-- SQL content not found");
101-
let requires_supabase = pgls_splinter::registry::rule_requires_supabase(R::METADATA.name);
102-
103-
// Extract description from SQL content metadata
104-
let description = extract_description_from_sql(sql_content);
100+
let registry_metadata = pgls_splinter::registry::get_rule_metadata(R::METADATA.name)
101+
.unwrap_or(pgls_splinter::registry::SplinterRuleMetadata {
102+
description: "Detects potential issues in your database schema.",
103+
remediation: "https://supabase.com/docs/guides/database/database-advisors",
104+
requires_supabase: false,
105+
});
105106

106107
group.insert(
107108
R::METADATA.name,
108109
SplinterRuleMetadata {
109110
metadata: R::METADATA,
110111
sql_content,
111-
requires_supabase,
112-
description,
112+
registry_metadata,
113113
},
114114
);
115115
}
116116
}
117-
118-
fn extract_description_from_sql(sql: &str) -> String {
119-
// Look for "-- meta: description = ..." in SQL content
120-
for line in sql.lines() {
121-
if let Some(desc_line) = line.strip_prefix("-- meta: description = ") {
122-
return desc_line.trim().to_string();
123-
}
124-
}
125-
"Detects potential issues in your database schema.".to_string()
126-
}

xtask/codegen/src/generate_splinter.rs

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -502,11 +502,41 @@ fn generate_registry(rules: &BTreeMap<String, SqlRuleMetadata>) -> Result<()> {
502502
})
503503
.collect();
504504

505+
// Generate match arms for metadata lookup (camelCase → SplinterRuleMetadata)
506+
let metadata_arms: Vec<_> = rules
507+
.values()
508+
.map(|rule| {
509+
let camel_name = &rule.name;
510+
let description = &rule.description;
511+
let remediation = &rule.remediation;
512+
let requires_supabase = rule.requires_supabase;
513+
514+
quote! {
515+
#camel_name => Some(SplinterRuleMetadata {
516+
description: #description,
517+
remediation: #remediation,
518+
requires_supabase: #requires_supabase,
519+
})
520+
}
521+
})
522+
.collect();
523+
505524
let content = quote! {
506525
//! Generated file, do not edit by hand, see `xtask/codegen`
507526
508527
use pgls_analyse::RegistryVisitor;
509528

529+
/// Metadata for a splinter rule
530+
#[derive(Debug, Clone, Copy)]
531+
pub struct SplinterRuleMetadata {
532+
/// Description of what the rule detects
533+
pub description: &'static str,
534+
/// URL to documentation/remediation guide
535+
pub remediation: &'static str,
536+
/// Whether this rule requires Supabase roles (anon, authenticated, service_role)
537+
pub requires_supabase: bool,
538+
}
539+
510540
/// Visit all splinter rules using the visitor pattern
511541
/// This is called during registry building to collect enabled rules
512542
pub fn visit_registry<V: RegistryVisitor>(registry: &mut V) {
@@ -535,6 +565,17 @@ fn generate_registry(rules: &BTreeMap<String, SqlRuleMetadata>) -> Result<()> {
535565
}
536566
}
537567

568+
/// Get metadata for a rule (camelCase name)
569+
/// Returns None if rule not found
570+
///
571+
/// This provides structured access to rule metadata without requiring SQL parsing
572+
pub fn get_rule_metadata(rule_name: &str) -> Option<SplinterRuleMetadata> {
573+
match rule_name {
574+
#( #metadata_arms, )*
575+
_ => None,
576+
}
577+
}
578+
538579
/// Map rule name from SQL result (snake_case) to diagnostic category
539580
/// Returns None if rule not found
540581
///
@@ -548,6 +589,7 @@ fn generate_registry(rules: &BTreeMap<String, SqlRuleMetadata>) -> Result<()> {
548589

549590
/// Check if a rule requires Supabase roles (anon, authenticated, service_role)
550591
/// Rules that require Supabase should be filtered out if these roles don't exist
592+
#[deprecated(note = "Use get_rule_metadata() instead")]
551593
pub fn rule_requires_supabase(rule_name: &str) -> bool {
552594
match rule_name {
553595
#( #supabase_arms, )*

0 commit comments

Comments
 (0)