Skip to content

Commit 229dc8f

Browse files
psteinroeclaude
andcommitted
refactor: embed splinter metadata directly in SplinterRule trait
Changed the architecture to make metadata accessible as trait constants instead of requiring registry lookups or SQL parsing: - Updated `SplinterRule` trait to include four associated constants: - `SQL_FILE_PATH`: Path to SQL file (changed from function to const) - `DESCRIPTION`: What the rule detects - `REMEDIATION`: URL to documentation - `REQUIRES_SUPABASE`: Whether rule needs Supabase roles - Updated codegen to generate these constants in each rule's impl block - Updated registry to provide metadata via qualified trait syntax: - `get_rule_metadata_fields()`: Returns tuple by calling trait constants - `get_rule_metadata()`: Returns struct by delegating to above - Updated docs codegen to use `get_rule_metadata_fields()` from registry Benefits: - Type-safe: Metadata defined with the rule type - No runtime string parsing - Single source of truth (SQL metadata comments) - Clean API: Registry provides runtime lookups via trait constants 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent dae4622 commit 229dc8f

27 files changed

+186
-207
lines changed

crates/pgls_splinter/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ pub async fn run_splinter(
103103
// Ensure all queries are wrapped for valid UNION ALL syntax
104104
let processed_queries: Vec<String> = sql_queries
105105
.iter()
106-
.map(|sql| {
106+
.map(|sql: &&str| {
107107
let trimmed = sql.trim();
108108
// Wrap in parentheses if not already wrapped
109109
if trimmed.starts_with('(') && trimmed.ends_with(')') {

crates/pgls_splinter/src/registry.rs

Lines changed: 14 additions & 109 deletions
Large diffs are not rendered by default.

crates/pgls_splinter/src/rule.rs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,5 +10,11 @@ use pgls_analyse::RuleMeta;
1010
#[doc = r" - Rule logic is in SQL files, not Rust"]
1111
pub trait SplinterRule: RuleMeta {
1212
#[doc = r" Path to the SQL file containing the rule query"]
13-
fn sql_file_path() -> &'static str;
13+
const SQL_FILE_PATH: &'static str;
14+
#[doc = r" Description of what the rule detects"]
15+
const DESCRIPTION: &'static str;
16+
#[doc = r" URL to documentation/remediation guide"]
17+
const REMEDIATION: &'static str;
18+
#[doc = r" Whether this rule requires Supabase roles (anon, authenticated, service_role)"]
19+
const REQUIRES_SUPABASE: bool;
1420
}

crates/pgls_splinter/src/rules/performance/auth_rls_initplan.rs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@
44
use crate::rule::SplinterRule;
55
::pgls_analyse::declare_rule! { # [doc = "/// # Auth RLS Initialization Plan\n///\n/// Detects if calls to \\`current_setting()\\` and \\`auth.<function>()\\` in RLS policies are being unnecessarily re-evaluated for each row\n/// \n/// **Note:** This rule requires Supabase roles (`anon`, `authenticated`, `service_role`). \n/// It will be automatically skipped if these roles don't exist in your database.\n///\n/// ## SQL Query\n///\n/// ```sql\n/// (\n/// with policies as (\n/// select\n/// nsp.nspname as schema_name,\n/// pb.tablename as table_name,\n/// pc.relrowsecurity as is_rls_active,\n/// polname as policy_name,\n/// polpermissive as is_permissive, -- if not, then restrictive\n/// (select array_agg(r::regrole) from unnest(polroles) as x(r)) as roles,\n/// case polcmd\n/// when 'r' then 'SELECT'\n/// when 'a' then 'INSERT'\n/// when 'w' then 'UPDATE'\n/// when 'd' then 'DELETE'\n/// when '*' then 'ALL'\n/// end as command,\n/// qual,\n/// with_check\n/// from\n/// pg_catalog.pg_policy pa\n/// join pg_catalog.pg_class pc\n/// on pa.polrelid = pc.oid\n/// join pg_catalog.pg_namespace nsp\n/// on pc.relnamespace = nsp.oid\n/// join pg_catalog.pg_policies pb\n/// on pc.relname = pb.tablename\n/// and nsp.nspname = pb.schemaname\n/// and pa.polname = pb.policyname\n/// )\n/// select\n/// 'auth_rls_initplan' as \"name!\",\n/// 'Auth RLS Initialization Plan' as \"title!\",\n/// 'WARN' as \"level!\",\n/// 'EXTERNAL' as \"facing!\",\n/// array['PERFORMANCE'] as \"categories!\",\n/// 'Detects if calls to \\`current_setting()\\` and \\`auth.<function>()\\` in RLS policies are being unnecessarily re-evaluated for each row' as \"description!\",\n/// format(\n/// 'Table \\`%s.%s\\` has a row level security policy \\`%s\\` that re-evaluates current_setting() or auth.<function>() for each row. This produces suboptimal query performance at scale. Resolve the issue by replacing \\`auth.<function>()\\` with \\`(select auth.<function>())\\`. See [docs](https://supabase.com/docs/guides/database/postgres/row-level-security#call-functions-with-select) for more info.',\n/// schema_name,\n/// table_name,\n/// policy_name\n/// ) as \"detail!\",\n/// 'https://supabase.com/docs/guides/database/database-linter?lint=0003_auth_rls_initplan' as \"remediation!\",\n/// jsonb_build_object(\n/// 'schema', schema_name,\n/// 'name', table_name,\n/// 'type', 'table'\n/// ) as \"metadata!\",\n/// format('auth_rls_init_plan_%s_%s_%s', schema_name, table_name, policy_name) as \"cache_key!\"\n/// from\n/// policies\n/// where\n/// is_rls_active\n/// -- NOTE: does not include realtime in support of monitoring policies on realtime.messages\n/// and schema_name not in (\n/// '_timescaledb_cache', '_timescaledb_catalog', '_timescaledb_config', '_timescaledb_internal', 'auth', 'cron', 'extensions', 'graphql', 'graphql_public', 'information_schema', 'net', 'pgmq', 'pgroonga', 'pgsodium', 'pgsodium_masks', 'pgtle', 'pgbouncer', 'pg_catalog', 'pgtle', 'repack', 'storage', 'supabase_functions', 'supabase_migrations', 'tiger', 'topology', 'vault'\n/// )\n/// and (\n/// -- Example: auth.uid()\n/// (\n/// qual like '%auth.uid()%'\n/// and lower(qual) not like '%select auth.uid()%'\n/// )\n/// or (\n/// qual like '%auth.jwt()%'\n/// and lower(qual) not like '%select auth.jwt()%'\n/// )\n/// or (\n/// qual like '%auth.role()%'\n/// and lower(qual) not like '%select auth.role()%'\n/// )\n/// or (\n/// qual like '%auth.email()%'\n/// and lower(qual) not like '%select auth.email()%'\n/// )\n/// or (\n/// qual like '%current\\_setting(%)%'\n/// and lower(qual) not like '%select current\\_setting(%)%'\n/// )\n/// or (\n/// with_check like '%auth.uid()%'\n/// and lower(with_check) not like '%select auth.uid()%'\n/// )\n/// or (\n/// with_check like '%auth.jwt()%'\n/// and lower(with_check) not like '%select auth.jwt()%'\n/// )\n/// or (\n/// with_check like '%auth.role()%'\n/// and lower(with_check) not like '%select auth.role()%'\n/// )\n/// or (\n/// with_check like '%auth.email()%'\n/// and lower(with_check) not like '%select auth.email()%'\n/// )\n/// or (\n/// with_check like '%current\\_setting(%)%'\n/// and lower(with_check) not like '%select current\\_setting(%)%'\n/// )\n/// ))\n/// ```\n///\n/// ## Configuration\n///\n/// Enable or disable this rule in your configuration:\n///\n/// ```json\n/// {\n/// \"splinter\": {\n/// \"rules\": {\n/// \"performance\": {\n/// \"authRlsInitplan\": \"warn\"\n/// }\n/// }\n/// }\n/// }\n/// ```\n///\n/// ## Remediation\n///\n/// See: <https://supabase.com/docs/guides/database/database-linter?lint=0003_auth_rls_initplan>"] pub AuthRlsInitplan { version : "1.0.0" , name : "authRlsInitplan" , severity : pgls_diagnostics :: Severity :: Warning , } }
66
impl SplinterRule for AuthRlsInitplan {
7-
fn sql_file_path() -> &'static str {
8-
"performance/auth_rls_initplan.sql"
9-
}
7+
const SQL_FILE_PATH: &'static str = "performance/auth_rls_initplan.sql";
8+
const DESCRIPTION: &'static str = "Detects if calls to \\`current_setting()\\` and \\`auth.<function>()\\` in RLS policies are being unnecessarily re-evaluated for each row";
9+
const REMEDIATION: &'static str =
10+
"https://supabase.com/docs/guides/database/database-linter?lint=0003_auth_rls_initplan";
11+
const REQUIRES_SUPABASE: bool = true;
1012
}

crates/pgls_splinter/src/rules/performance/duplicate_index.rs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@
44
use crate::rule::SplinterRule;
55
::pgls_analyse::declare_rule! { # [doc = "/// # Duplicate Index\n///\n/// Detects cases where two ore more identical indexes exist.\n///\n/// ## SQL Query\n///\n/// ```sql\n/// (\n/// select\n/// 'duplicate_index' as \"name!\",\n/// 'Duplicate Index' as \"title!\",\n/// 'WARN' as \"level!\",\n/// 'EXTERNAL' as \"facing!\",\n/// array['PERFORMANCE'] as \"categories!\",\n/// 'Detects cases where two ore more identical indexes exist.' as \"description!\",\n/// format(\n/// 'Table \\`%s.%s\\` has identical indexes %s. Drop all except one of them',\n/// n.nspname,\n/// c.relname,\n/// array_agg(pi.indexname order by pi.indexname)\n/// ) as \"detail!\",\n/// 'https://supabase.com/docs/guides/database/database-linter?lint=0009_duplicate_index' as \"remediation!\",\n/// jsonb_build_object(\n/// 'schema', n.nspname,\n/// 'name', c.relname,\n/// 'type', case\n/// when c.relkind = 'r' then 'table'\n/// when c.relkind = 'm' then 'materialized view'\n/// else 'ERROR'\n/// end,\n/// 'indexes', array_agg(pi.indexname order by pi.indexname)\n/// ) as \"metadata!\",\n/// format(\n/// 'duplicate_index_%s_%s_%s',\n/// n.nspname,\n/// c.relname,\n/// array_agg(pi.indexname order by pi.indexname)\n/// ) as \"cache_key!\"\n/// from\n/// pg_catalog.pg_indexes pi\n/// join pg_catalog.pg_namespace n\n/// on n.nspname = pi.schemaname\n/// join pg_catalog.pg_class c\n/// on pi.tablename = c.relname\n/// and n.oid = c.relnamespace\n/// left join pg_catalog.pg_depend dep\n/// on c.oid = dep.objid\n/// and dep.deptype = 'e'\n/// where\n/// c.relkind in ('r', 'm') -- tables and materialized views\n/// and n.nspname not in (\n/// '_timescaledb_cache', '_timescaledb_catalog', '_timescaledb_config', '_timescaledb_internal', 'auth', 'cron', 'extensions', 'graphql', 'graphql_public', 'information_schema', 'net', 'pgmq', 'pgroonga', 'pgsodium', 'pgsodium_masks', 'pgtle', 'pgbouncer', 'pg_catalog', 'pgtle', 'realtime', 'repack', 'storage', 'supabase_functions', 'supabase_migrations', 'tiger', 'topology', 'vault'\n/// )\n/// and dep.objid is null -- exclude tables owned by extensions\n/// group by\n/// n.nspname,\n/// c.relkind,\n/// c.relname,\n/// replace(pi.indexdef, pi.indexname, '')\n/// having\n/// count(*) > 1)\n/// ```\n///\n/// ## Configuration\n///\n/// Enable or disable this rule in your configuration:\n///\n/// ```json\n/// {\n/// \"splinter\": {\n/// \"rules\": {\n/// \"performance\": {\n/// \"duplicateIndex\": \"warn\"\n/// }\n/// }\n/// }\n/// }\n/// ```\n///\n/// ## Remediation\n///\n/// See: <https://supabase.com/docs/guides/database/database-linter?lint=0009_duplicate_index>"] pub DuplicateIndex { version : "1.0.0" , name : "duplicateIndex" , severity : pgls_diagnostics :: Severity :: Warning , } }
66
impl SplinterRule for DuplicateIndex {
7-
fn sql_file_path() -> &'static str {
8-
"performance/duplicate_index.sql"
9-
}
7+
const SQL_FILE_PATH: &'static str = "performance/duplicate_index.sql";
8+
const DESCRIPTION: &'static str = "Detects cases where two ore more identical indexes exist.";
9+
const REMEDIATION: &'static str =
10+
"https://supabase.com/docs/guides/database/database-linter?lint=0009_duplicate_index";
11+
const REQUIRES_SUPABASE: bool = false;
1012
}

crates/pgls_splinter/src/rules/performance/multiple_permissive_policies.rs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44
use crate::rule::SplinterRule;
55
::pgls_analyse::declare_rule! { # [doc = "/// # Multiple Permissive Policies\n///\n/// 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.\n///\n/// ## SQL Query\n///\n/// ```sql\n/// (\n/// select\n/// 'multiple_permissive_policies' as \"name!\",\n/// 'Multiple Permissive Policies' as \"title!\",\n/// 'WARN' as \"level!\",\n/// 'EXTERNAL' as \"facing!\",\n/// array['PERFORMANCE'] as \"categories!\",\n/// '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.' as \"description!\",\n/// format(\n/// 'Table \\`%s.%s\\` has multiple permissive policies for role \\`%s\\` for action \\`%s\\`. Policies include \\`%s\\`',\n/// n.nspname,\n/// c.relname,\n/// r.rolname,\n/// act.cmd,\n/// array_agg(p.polname order by p.polname)\n/// ) as \"detail!\",\n/// 'https://supabase.com/docs/guides/database/database-linter?lint=0006_multiple_permissive_policies' as \"remediation!\",\n/// jsonb_build_object(\n/// 'schema', n.nspname,\n/// 'name', c.relname,\n/// 'type', 'table'\n/// ) as \"metadata!\",\n/// format(\n/// 'multiple_permissive_policies_%s_%s_%s_%s',\n/// n.nspname,\n/// c.relname,\n/// r.rolname,\n/// act.cmd\n/// ) as \"cache_key!\"\n/// from\n/// pg_catalog.pg_policy p\n/// join pg_catalog.pg_class c\n/// on p.polrelid = c.oid\n/// join pg_catalog.pg_namespace n\n/// on c.relnamespace = n.oid\n/// join pg_catalog.pg_roles r\n/// on p.polroles @> array[r.oid]\n/// or p.polroles = array[0::oid]\n/// left join pg_catalog.pg_depend dep\n/// on c.oid = dep.objid\n/// and dep.deptype = 'e',\n/// lateral (\n/// select x.cmd\n/// from unnest((\n/// select\n/// case p.polcmd\n/// when 'r' then array['SELECT']\n/// when 'a' then array['INSERT']\n/// when 'w' then array['UPDATE']\n/// when 'd' then array['DELETE']\n/// when '*' then array['SELECT', 'INSERT', 'UPDATE', 'DELETE']\n/// else array['ERROR']\n/// end as actions\n/// )) x(cmd)\n/// ) act(cmd)\n/// where\n/// c.relkind = 'r' -- regular tables\n/// and p.polpermissive -- policy is permissive\n/// and n.nspname not in (\n/// '_timescaledb_cache', '_timescaledb_catalog', '_timescaledb_config', '_timescaledb_internal', 'auth', 'cron', 'extensions', 'graphql', 'graphql_public', 'information_schema', 'net', 'pgmq', 'pgroonga', 'pgsodium', 'pgsodium_masks', 'pgtle', 'pgbouncer', 'pg_catalog', 'pgtle', 'realtime', 'repack', 'storage', 'supabase_functions', 'supabase_migrations', 'tiger', 'topology', 'vault'\n/// )\n/// and r.rolname not like 'pg_%'\n/// and r.rolname not like 'supabase%admin'\n/// and not r.rolbypassrls\n/// and dep.objid is null -- exclude tables owned by extensions\n/// group by\n/// n.nspname,\n/// c.relname,\n/// r.rolname,\n/// act.cmd\n/// having\n/// count(1) > 1)\n/// ```\n///\n/// ## Configuration\n///\n/// Enable or disable this rule in your configuration:\n///\n/// ```json\n/// {\n/// \"splinter\": {\n/// \"rules\": {\n/// \"performance\": {\n/// \"multiplePermissivePolicies\": \"warn\"\n/// }\n/// }\n/// }\n/// }\n/// ```\n///\n/// ## Remediation\n///\n/// See: <https://supabase.com/docs/guides/database/database-linter?lint=0006_multiple_permissive_policies>"] pub MultiplePermissivePolicies { version : "1.0.0" , name : "multiplePermissivePolicies" , severity : pgls_diagnostics :: Severity :: Warning , } }
66
impl SplinterRule for MultiplePermissivePolicies {
7-
fn sql_file_path() -> &'static str {
8-
"performance/multiple_permissive_policies.sql"
9-
}
7+
const SQL_FILE_PATH: &'static str = "performance/multiple_permissive_policies.sql";
8+
const DESCRIPTION: &'static str = "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.";
9+
const REMEDIATION: &'static str = "https://supabase.com/docs/guides/database/database-linter?lint=0006_multiple_permissive_policies";
10+
const REQUIRES_SUPABASE: bool = false;
1011
}

crates/pgls_splinter/src/rules/performance/no_primary_key.rs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@
44
use crate::rule::SplinterRule;
55
::pgls_analyse::declare_rule! { # [doc = "/// # No Primary Key\n///\n/// Detects if a table does not have a primary key. Tables without a primary key can be inefficient to interact with at scale.\n///\n/// ## SQL Query\n///\n/// ```sql\n/// (\n/// select\n/// 'no_primary_key' as \"name!\",\n/// 'No Primary Key' as \"title!\",\n/// 'INFO' as \"level!\",\n/// 'EXTERNAL' as \"facing!\",\n/// array['PERFORMANCE'] as \"categories!\",\n/// 'Detects if a table does not have a primary key. Tables without a primary key can be inefficient to interact with at scale.' as \"description!\",\n/// format(\n/// 'Table \\`%s.%s\\` does not have a primary key',\n/// pgns.nspname,\n/// pgc.relname\n/// ) as \"detail!\",\n/// 'https://supabase.com/docs/guides/database/database-linter?lint=0004_no_primary_key' as \"remediation!\",\n/// jsonb_build_object(\n/// 'schema', pgns.nspname,\n/// 'name', pgc.relname,\n/// 'type', 'table'\n/// ) as \"metadata!\",\n/// format(\n/// 'no_primary_key_%s_%s',\n/// pgns.nspname,\n/// pgc.relname\n/// ) as \"cache_key!\"\n/// from\n/// pg_catalog.pg_class pgc\n/// join pg_catalog.pg_namespace pgns\n/// on pgns.oid = pgc.relnamespace\n/// left join pg_catalog.pg_index pgi\n/// on pgi.indrelid = pgc.oid\n/// left join pg_catalog.pg_depend dep\n/// on pgc.oid = dep.objid\n/// and dep.deptype = 'e'\n/// where\n/// pgc.relkind = 'r' -- regular tables\n/// and pgns.nspname not in (\n/// '_timescaledb_cache', '_timescaledb_catalog', '_timescaledb_config', '_timescaledb_internal', 'auth', 'cron', 'extensions', 'graphql', 'graphql_public', 'information_schema', 'net', 'pgmq', 'pgroonga', 'pgsodium', 'pgsodium_masks', 'pgtle', 'pgbouncer', 'pg_catalog', 'pgtle', 'realtime', 'repack', 'storage', 'supabase_functions', 'supabase_migrations', 'tiger', 'topology', 'vault'\n/// )\n/// and dep.objid is null -- exclude tables owned by extensions\n/// group by\n/// pgc.oid,\n/// pgns.nspname,\n/// pgc.relname\n/// having\n/// max(coalesce(pgi.indisprimary, false)::int) = 0)\n/// ```\n///\n/// ## Configuration\n///\n/// Enable or disable this rule in your configuration:\n///\n/// ```json\n/// {\n/// \"splinter\": {\n/// \"rules\": {\n/// \"performance\": {\n/// \"noPrimaryKey\": \"warn\"\n/// }\n/// }\n/// }\n/// }\n/// ```\n///\n/// ## Remediation\n///\n/// See: <https://supabase.com/docs/guides/database/database-linter?lint=0004_no_primary_key>"] pub NoPrimaryKey { version : "1.0.0" , name : "noPrimaryKey" , severity : pgls_diagnostics :: Severity :: Information , } }
66
impl SplinterRule for NoPrimaryKey {
7-
fn sql_file_path() -> &'static str {
8-
"performance/no_primary_key.sql"
9-
}
7+
const SQL_FILE_PATH: &'static str = "performance/no_primary_key.sql";
8+
const DESCRIPTION: &'static str = "Detects if a table does not have a primary key. Tables without a primary key can be inefficient to interact with at scale.";
9+
const REMEDIATION: &'static str =
10+
"https://supabase.com/docs/guides/database/database-linter?lint=0004_no_primary_key";
11+
const REQUIRES_SUPABASE: bool = false;
1012
}

0 commit comments

Comments
 (0)