From 561328862bd89b44d0fc72d511555a50d228387d Mon Sep 17 00:00:00 2001 From: avallete Date: Tue, 20 Oct 2026 16:57:51 +0200 Subject: [PATCH 01/13] perf(lints): deparse each policy once in auth_rls_initplan The policies CTE reads qual and with_check from pg_policies, which is pg_get_expr(). Inlined, the planner deparsed each expression once per LIKE test (up to 10 per policy). Materializing the CTE deparses it once. carbon (1.5k policies): 172 -> 32 ms. synthetic 2k tables: 56 -> 43 ms. Rows identical on both. Co-Authored-By: Claude Opus 5.5 --- lints/0003_auth_rls_initplan.sql | 2 +- splinter.sql | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lints/0003_auth_rls_initplan.sql b/lints/0003_auth_rls_initplan.sql index ee53e6be..1b1cd4af 100644 --- a/lints/0003_auth_rls_initplan.sql +++ b/lints/0003_auth_rls_initplan.sql @@ -1,6 +1,6 @@ create view lint."0003_auth_rls_initplan" as -with policies as ( +with policies as materialized ( select nsp.nspname as schema_name, pb.tablename as table_name, diff --git a/splinter.sql b/splinter.sql index 46bb331e..5f68268d 100644 --- a/splinter.sql +++ b/splinter.sql @@ -189,7 +189,7 @@ group by c.oid) union all ( -with policies as ( +with policies as materialized ( select nsp.nspname as schema_name, pb.tablename as table_name, From 2067c45c0cba2998213ad7b1c4f810deeb319b79 Mon Sep 17 00:00:00 2001 From: avallete Date: Tue, 20 Oct 2026 16:59:32 +0200 Subject: [PATCH 02/13] perf(lints): read policy expressions from pg_policy in rls_references_user_metadata The policies CTE joined pg_policies back to pg_policy by schema, table and policy name only to read qual and with_check. Calling pg_get_expr on pg_policy directly gives the same text (pg_policies is defined that way) without the per-policy name lookups. carbon: 71 -> 19 ms, 24.7k -> 8.0k buffers. synthetic 2k tables: 13.8 -> 13.0 ms, 1.8k -> 0.9k buffers. Rows identical on both. Co-Authored-By: Claude Opus 5.5 --- lints/0015_rls_references_user_metadata.sql | 11 ++++------- splinter.sql | 11 ++++------- 2 files changed, 8 insertions(+), 14 deletions(-) diff --git a/lints/0015_rls_references_user_metadata.sql b/lints/0015_rls_references_user_metadata.sql index f5dc8fa2..a8108846 100644 --- a/lints/0015_rls_references_user_metadata.sql +++ b/lints/0015_rls_references_user_metadata.sql @@ -3,20 +3,17 @@ create view lint."0015_rls_references_user_metadata" as with policies as ( select nsp.nspname as schema_name, - pb.tablename as table_name, + pc.relname as table_name, polname as policy_name, - qual, - with_check + -- what pg_policies reports as qual and with_check + pg_catalog.pg_get_expr(pa.polqual, pa.polrelid) as qual, + pg_catalog.pg_get_expr(pa.polwithcheck, pa.polrelid) as with_check from pg_catalog.pg_policy pa join pg_catalog.pg_class pc on pa.polrelid = pc.oid join pg_catalog.pg_namespace nsp on pc.relnamespace = nsp.oid - join pg_catalog.pg_policies pb - on pc.relname = pb.tablename - and nsp.nspname = pb.schemaname - and pa.polname = pb.policyname ) select 'rls_references_user_metadata' as name, diff --git a/splinter.sql b/splinter.sql index 5f68268d..a5109648 100644 --- a/splinter.sql +++ b/splinter.sql @@ -782,20 +782,17 @@ union all with policies as ( select nsp.nspname as schema_name, - pb.tablename as table_name, + pc.relname as table_name, polname as policy_name, - qual, - with_check + -- what pg_policies reports as qual and with_check + pg_catalog.pg_get_expr(pa.polqual, pa.polrelid) as qual, + pg_catalog.pg_get_expr(pa.polwithcheck, pa.polrelid) as with_check from pg_catalog.pg_policy pa join pg_catalog.pg_class pc on pa.polrelid = pc.oid join pg_catalog.pg_namespace nsp on pc.relnamespace = nsp.oid - join pg_catalog.pg_policies pb - on pc.relname = pb.tablename - and nsp.nspname = pb.schemaname - and pa.polname = pb.policyname ) select 'rls_references_user_metadata' as name, From d3db70dfbed47bfafb778e7ce1f3c419cd61a175 Mon Sep 17 00:00:00 2001 From: avallete Date: Tue, 20 Oct 2026 17:00:20 +0200 Subject: [PATCH 03/13] perf(lints): read policy expressions from pg_policy in rls_policy_always_true Same change as rls_references_user_metadata: pg_get_expr on pg_policy instead of joining pg_policies back by schema, table and policy name. synthetic 2k tables: 18.1 -> 12.6 ms, 20.2k -> 5.6k buffers. carbon: 23.2 -> 21.6 ms, 17.5k -> 4.2k buffers. Rows identical on both. On carbon the planner now seq-scans pg_class once per non-system schema (14 loops), which the benchmark's plan guard reports as a repeated catalog scan. It is bounded by the schema count, not by rows, and the query reads 4x fewer buffers overall. Materializing the CTE avoids it but deparses every policy before filtering and loses the synthetic speedup. Co-Authored-By: Claude Opus 5.5 --- lints/0024_rls_policy_always_true.sql | 15 ++++++--------- splinter.sql | 15 ++++++--------- 2 files changed, 12 insertions(+), 18 deletions(-) diff --git a/lints/0024_rls_policy_always_true.sql b/lints/0024_rls_policy_always_true.sql index 7e05efaf..2e5a389a 100644 --- a/lints/0024_rls_policy_always_true.sql +++ b/lints/0024_rls_policy_always_true.sql @@ -5,7 +5,7 @@ create view lint."0024_rls_policy_always_true" as with policies as ( select nsp.nspname as schema_name, - pb.tablename as table_name, + pc.relname as table_name, pc.relrowsecurity as is_rls_active, pa.polname as policy_name, pa.polpermissive as is_permissive, @@ -18,21 +18,18 @@ with policies as ( when 'd' then 'DELETE' when '*' then 'ALL' end as command, - pb.qual, - pb.with_check, + -- what pg_policies reports as qual and with_check + pg_catalog.pg_get_expr(pa.polqual, pa.polrelid) as qual, + pg_catalog.pg_get_expr(pa.polwithcheck, pa.polrelid) as with_check, -- Normalize expressions by removing whitespace and lowercasing - replace(replace(replace(lower(coalesce(pb.qual, '')), ' ', ''), E'\n', ''), E'\t', '') as normalized_qual, - replace(replace(replace(lower(coalesce(pb.with_check, '')), ' ', ''), E'\n', ''), E'\t', '') as normalized_with_check + replace(replace(replace(lower(coalesce(pg_catalog.pg_get_expr(pa.polqual, pa.polrelid), '')), ' ', ''), E'\n', ''), E'\t', '') as normalized_qual, + replace(replace(replace(lower(coalesce(pg_catalog.pg_get_expr(pa.polwithcheck, pa.polrelid), '')), ' ', ''), E'\n', ''), E'\t', '') as normalized_with_check from pg_catalog.pg_policy pa join pg_catalog.pg_class pc on pa.polrelid = pc.oid join pg_catalog.pg_namespace nsp on pc.relnamespace = nsp.oid - join pg_catalog.pg_policies pb - on pc.relname = pb.tablename - and nsp.nspname = pb.schemaname - and pa.polname = pb.policyname where pc.relkind = 'r' -- regular tables and nsp.nspname not in ( diff --git a/splinter.sql b/splinter.sql index a5109648..a48692ed 100644 --- a/splinter.sql +++ b/splinter.sql @@ -1296,7 +1296,7 @@ union all with policies as ( select nsp.nspname as schema_name, - pb.tablename as table_name, + pc.relname as table_name, pc.relrowsecurity as is_rls_active, pa.polname as policy_name, pa.polpermissive as is_permissive, @@ -1309,21 +1309,18 @@ with policies as ( when 'd' then 'DELETE' when '*' then 'ALL' end as command, - pb.qual, - pb.with_check, + -- what pg_policies reports as qual and with_check + pg_catalog.pg_get_expr(pa.polqual, pa.polrelid) as qual, + pg_catalog.pg_get_expr(pa.polwithcheck, pa.polrelid) as with_check, -- Normalize expressions by removing whitespace and lowercasing - replace(replace(replace(lower(coalesce(pb.qual, '')), ' ', ''), E'\n', ''), E'\t', '') as normalized_qual, - replace(replace(replace(lower(coalesce(pb.with_check, '')), ' ', ''), E'\n', ''), E'\t', '') as normalized_with_check + replace(replace(replace(lower(coalesce(pg_catalog.pg_get_expr(pa.polqual, pa.polrelid), '')), ' ', ''), E'\n', ''), E'\t', '') as normalized_qual, + replace(replace(replace(lower(coalesce(pg_catalog.pg_get_expr(pa.polwithcheck, pa.polrelid), '')), ' ', ''), E'\n', ''), E'\t', '') as normalized_with_check from pg_catalog.pg_policy pa join pg_catalog.pg_class pc on pa.polrelid = pc.oid join pg_catalog.pg_namespace nsp on pc.relnamespace = nsp.oid - join pg_catalog.pg_policies pb - on pc.relname = pb.tablename - and nsp.nspname = pb.schemaname - and pa.polname = pb.policyname where pc.relkind = 'r' -- regular tables and nsp.nspname not in ( From 68feff5eef115ebb485a0bb573b7be43d0791af0 Mon Sep 17 00:00:00 2001 From: avallete Date: Tue, 20 Oct 2026 17:00:38 +0200 Subject: [PATCH 04/13] perf(lints): read policy expressions from pg_policy in auth_rls_initplan Drop the join back to pg_policies by schema, table and policy name; call pg_get_expr on pg_policy directly, which is how pg_policies defines qual and with_check. synthetic 2k tables: 25.8 -> 21.7 ms, 10.0k -> 0.9k buffers. carbon: 17.0 -> 13.1 ms, 14.3k -> 0.8k buffers. Rows identical on both. Co-Authored-By: Claude Opus 5.5 --- lints/0003_auth_rls_initplan.sql | 11 ++++------- splinter.sql | 11 ++++------- 2 files changed, 8 insertions(+), 14 deletions(-) diff --git a/lints/0003_auth_rls_initplan.sql b/lints/0003_auth_rls_initplan.sql index 1b1cd4af..527478bf 100644 --- a/lints/0003_auth_rls_initplan.sql +++ b/lints/0003_auth_rls_initplan.sql @@ -3,7 +3,7 @@ create view lint."0003_auth_rls_initplan" as with policies as materialized ( select nsp.nspname as schema_name, - pb.tablename as table_name, + pc.relname as table_name, pc.relrowsecurity as is_rls_active, polname as policy_name, polpermissive as is_permissive, -- if not, then restrictive @@ -15,18 +15,15 @@ with policies as materialized ( when 'd' then 'DELETE' when '*' then 'ALL' end as command, - qual, - with_check + -- what pg_policies reports as qual and with_check + pg_catalog.pg_get_expr(pa.polqual, pa.polrelid) as qual, + pg_catalog.pg_get_expr(pa.polwithcheck, pa.polrelid) as with_check from pg_catalog.pg_policy pa join pg_catalog.pg_class pc on pa.polrelid = pc.oid join pg_catalog.pg_namespace nsp on pc.relnamespace = nsp.oid - join pg_catalog.pg_policies pb - on pc.relname = pb.tablename - and nsp.nspname = pb.schemaname - and pa.polname = pb.policyname ) select 'auth_rls_initplan' as name, diff --git a/splinter.sql b/splinter.sql index a48692ed..4f7b7cce 100644 --- a/splinter.sql +++ b/splinter.sql @@ -192,7 +192,7 @@ union all with policies as materialized ( select nsp.nspname as schema_name, - pb.tablename as table_name, + pc.relname as table_name, pc.relrowsecurity as is_rls_active, polname as policy_name, polpermissive as is_permissive, -- if not, then restrictive @@ -204,18 +204,15 @@ with policies as materialized ( when 'd' then 'DELETE' when '*' then 'ALL' end as command, - qual, - with_check + -- what pg_policies reports as qual and with_check + pg_catalog.pg_get_expr(pa.polqual, pa.polrelid) as qual, + pg_catalog.pg_get_expr(pa.polwithcheck, pa.polrelid) as with_check from pg_catalog.pg_policy pa join pg_catalog.pg_class pc on pa.polrelid = pc.oid join pg_catalog.pg_namespace nsp on pc.relnamespace = nsp.oid - join pg_catalog.pg_policies pb - on pc.relname = pb.tablename - and nsp.nspname = pb.schemaname - and pa.polname = pb.policyname ) select 'auth_rls_initplan' as name, From 8c923b09e5168006485c39306021a52bdb46cb34 Mon Sep 17 00:00:00 2001 From: avallete Date: Tue, 20 Oct 2026 17:01:18 +0200 Subject: [PATCH 05/13] perf(lints): inline pg_stat_user_indexes in unused_index The view evaluates pg_stat_get_numscans for every pg_class row before its joins narrow to indexes, and the lint joined pg_index a second time. The inlined query follows the view's definition (identical on PG 15 and 17; its left join to pg_namespace acts as an inner join under the schema filters) and reads idx_scan only for non-unique, non-primary indexes. synthetic 2k tables: 47.7 -> 44.7 ms, 58.4k -> 42.7k buffers. carbon: 11.4 -> 10.3 ms, 15.5k -> 9.7k buffers. Rows identical on both. Co-Authored-By: Claude Opus 5.5 --- lints/0005_unused_index.sql | 36 ++++++++++++++++++++++-------------- splinter.sql | 36 ++++++++++++++++++++++-------------- 2 files changed, 44 insertions(+), 28 deletions(-) diff --git a/lints/0005_unused_index.sql b/lints/0005_unused_index.sql index 4d599a89..bfa8cabc 100644 --- a/lints/0005_unused_index.sql +++ b/lints/0005_unused_index.sql @@ -9,36 +9,44 @@ select 'Detects if an index has never been used and may be a candidate for removal.' as description, format( 'Index \`%s\` on table \`%s.%s\` has not been used', - psui.indexrelname, - psui.schemaname, - psui.relname + i.relname, + n.nspname, + c.relname ) as detail, 'https://supabase.com/docs/guides/database/database-linter?lint=0005_unused_index' as remediation, jsonb_build_object( - 'schema', psui.schemaname, - 'name', psui.relname, + 'schema', n.nspname, + 'name', c.relname, 'type', 'table' ) as metadata, format( 'unused_index_%s_%s_%s', - psui.schemaname, - psui.relname, - psui.indexrelname + n.nspname, + c.relname, + i.relname ) as cache_key from - pg_catalog.pg_stat_user_indexes psui - join pg_catalog.pg_index pi - on psui.indexrelid = pi.indexrelid + -- pg_stat_user_indexes, inlined so idx_scan is read only for the candidate indexes + pg_catalog.pg_index pi + join pg_catalog.pg_class i + on i.oid = pi.indexrelid + join pg_catalog.pg_class c + on c.oid = pi.indrelid + and c.relkind in ('r', 't', 'm') + join pg_catalog.pg_namespace n + on n.oid = c.relnamespace left join pg_catalog.pg_depend dep - on psui.relid = dep.objid + on c.oid = dep.objid and dep.deptype = 'e' and dep.classid = 'pg_catalog.pg_class'::regclass where - psui.idx_scan = 0 + pg_catalog.pg_stat_get_numscans(i.oid) = 0 and not pi.indisunique and not pi.indisprimary and dep.objid is null -- exclude tables owned by extensions - and psui.schemaname not in ( + and n.nspname not in ('pg_catalog', 'information_schema') + and n.nspname !~ '^pg_toast' + and n.nspname not in ( '_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', 'realtime', 'repack', 'storage', 'supabase_functions', 'supabase_migrations', 'tiger', 'topology', 'vault' ); diff --git a/splinter.sql b/splinter.sql index 4f7b7cce..2237d3ad 100644 --- a/splinter.sql +++ b/splinter.sql @@ -343,37 +343,45 @@ select 'Detects if an index has never been used and may be a candidate for removal.' as description, format( 'Index \`%s\` on table \`%s.%s\` has not been used', - psui.indexrelname, - psui.schemaname, - psui.relname + i.relname, + n.nspname, + c.relname ) as detail, 'https://supabase.com/docs/guides/database/database-linter?lint=0005_unused_index' as remediation, jsonb_build_object( - 'schema', psui.schemaname, - 'name', psui.relname, + 'schema', n.nspname, + 'name', c.relname, 'type', 'table' ) as metadata, format( 'unused_index_%s_%s_%s', - psui.schemaname, - psui.relname, - psui.indexrelname + n.nspname, + c.relname, + i.relname ) as cache_key from - pg_catalog.pg_stat_user_indexes psui - join pg_catalog.pg_index pi - on psui.indexrelid = pi.indexrelid + -- pg_stat_user_indexes, inlined so idx_scan is read only for the candidate indexes + pg_catalog.pg_index pi + join pg_catalog.pg_class i + on i.oid = pi.indexrelid + join pg_catalog.pg_class c + on c.oid = pi.indrelid + and c.relkind in ('r', 't', 'm') + join pg_catalog.pg_namespace n + on n.oid = c.relnamespace left join pg_catalog.pg_depend dep - on psui.relid = dep.objid + on c.oid = dep.objid and dep.deptype = 'e' and dep.classid = 'pg_catalog.pg_class'::regclass where - psui.idx_scan = 0 + pg_catalog.pg_stat_get_numscans(i.oid) = 0 and not pi.indisunique and not pi.indisprimary and dep.objid is null -- exclude tables owned by extensions - and psui.schemaname not in ( + and n.nspname not in ('pg_catalog', 'information_schema') + and n.nspname !~ '^pg_toast' + and n.nspname not in ( '_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', 'realtime', 'repack', 'storage', 'supabase_functions', 'supabase_migrations', 'tiger', 'topology', 'vault' )) union all From c338e99fc8da2ad6c2153f6113df83a00ba90a70 Mon Sep 17 00:00:00 2001 From: avallete Date: Tue, 20 Oct 2026 17:05:13 +0200 Subject: [PATCH 06/13] refactor(lints): order auth_rls_initplan output Materializing the policies CTE changed the incidental row order the lint returned (it had no ORDER BY, so the order followed whichever plan the planner picked). Order by schema, table and policy, like rls_policy_always_true and the pg_graphql lints. The row set is unchanged; the test's expected output differs only in row order. Co-Authored-By: Claude Opus 5.5 --- lints/0003_auth_rls_initplan.sql | 6 +++++- splinter.sql | 6 +++++- test/expected/0003_auth_rls_initplan.out | 6 +++--- 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/lints/0003_auth_rls_initplan.sql b/lints/0003_auth_rls_initplan.sql index 527478bf..4f49a54e 100644 --- a/lints/0003_auth_rls_initplan.sql +++ b/lints/0003_auth_rls_initplan.sql @@ -95,4 +95,8 @@ where with_check like '%current\_setting(%)%' and lower(with_check) not like '%select current\_setting(%)%' ) - ); + ) +order by + schema_name, + table_name, + policy_name; diff --git a/splinter.sql b/splinter.sql index 2237d3ad..f397da71 100644 --- a/splinter.sql +++ b/splinter.sql @@ -284,7 +284,11 @@ where with_check like '%current\_setting(%)%' and lower(with_check) not like '%select current\_setting(%)%' ) - )) + ) +order by + schema_name, + table_name, + policy_name) union all ( select diff --git a/test/expected/0003_auth_rls_initplan.out b/test/expected/0003_auth_rls_initplan.out index b5de9a4a..39ad35c3 100644 --- a/test/expected/0003_auth_rls_initplan.out +++ b/test/expected/0003_auth_rls_initplan.out @@ -67,11 +67,11 @@ begin; select * from lint."0003_auth_rls_initplan"; name | title | level | facing | categories | description | detail | remediation | metadata | cache_key -------------------+------------------------------+-------+----------+---------------+---------------------------------------------------------------------------------------------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+---------------------------------------------------------------------------------------+------------------------------------------------------+---------------------------------------------------------- - auth_rls_initplan | Auth RLS Initialization Plan | WARN | EXTERNAL | {PERFORMANCE} | Detects if calls to \`current_setting()\` and \`auth.()\` in RLS policies are being unnecessarily re-evaluated for each row | Table \`public.foo\` has a row level security policy \`bad_policy_uid\` that re-evaluates current_setting() or auth.() for each row. This produces suboptimal query performance at scale. Resolve the issue by replacing \`auth.()\` with \`(select auth.())\`. See [docs](https://supabase.com/docs/guides/database/postgres/row-level-security#call-functions-with-select) for more info. | https://supabase.com/docs/guides/database/database-linter?lint=0003_auth_rls_initplan | {"name": "foo", "type": "table", "schema": "public"} | auth_rls_init_plan_public_foo_bad_policy_uid + auth_rls_initplan | Auth RLS Initialization Plan | WARN | EXTERNAL | {PERFORMANCE} | Detects if calls to \`current_setting()\` and \`auth.()\` in RLS policies are being unnecessarily re-evaluated for each row | Table \`public.foo\` has a row level security policy \`bad_policy_current_setting\` that re-evaluates current_setting() or auth.() for each row. This produces suboptimal query performance at scale. Resolve the issue by replacing \`auth.()\` with \`(select auth.())\`. See [docs](https://supabase.com/docs/guides/database/postgres/row-level-security#call-functions-with-select) for more info. | https://supabase.com/docs/guides/database/database-linter?lint=0003_auth_rls_initplan | {"name": "foo", "type": "table", "schema": "public"} | auth_rls_init_plan_public_foo_bad_policy_current_setting + auth_rls_initplan | Auth RLS Initialization Plan | WARN | EXTERNAL | {PERFORMANCE} | Detects if calls to \`current_setting()\` and \`auth.()\` in RLS policies are being unnecessarily re-evaluated for each row | Table \`public.foo\` has a row level security policy \`bad_policy_email\` that re-evaluates current_setting() or auth.() for each row. This produces suboptimal query performance at scale. Resolve the issue by replacing \`auth.()\` with \`(select auth.())\`. See [docs](https://supabase.com/docs/guides/database/postgres/row-level-security#call-functions-with-select) for more info. | https://supabase.com/docs/guides/database/database-linter?lint=0003_auth_rls_initplan | {"name": "foo", "type": "table", "schema": "public"} | auth_rls_init_plan_public_foo_bad_policy_email auth_rls_initplan | Auth RLS Initialization Plan | WARN | EXTERNAL | {PERFORMANCE} | Detects if calls to \`current_setting()\` and \`auth.()\` in RLS policies are being unnecessarily re-evaluated for each row | Table \`public.foo\` has a row level security policy \`bad_policy_jwt\` that re-evaluates current_setting() or auth.() for each row. This produces suboptimal query performance at scale. Resolve the issue by replacing \`auth.()\` with \`(select auth.())\`. See [docs](https://supabase.com/docs/guides/database/postgres/row-level-security#call-functions-with-select) for more info. | https://supabase.com/docs/guides/database/database-linter?lint=0003_auth_rls_initplan | {"name": "foo", "type": "table", "schema": "public"} | auth_rls_init_plan_public_foo_bad_policy_jwt auth_rls_initplan | Auth RLS Initialization Plan | WARN | EXTERNAL | {PERFORMANCE} | Detects if calls to \`current_setting()\` and \`auth.()\` in RLS policies are being unnecessarily re-evaluated for each row | Table \`public.foo\` has a row level security policy \`bad_policy_role\` that re-evaluates current_setting() or auth.() for each row. This produces suboptimal query performance at scale. Resolve the issue by replacing \`auth.()\` with \`(select auth.())\`. See [docs](https://supabase.com/docs/guides/database/postgres/row-level-security#call-functions-with-select) for more info. | https://supabase.com/docs/guides/database/database-linter?lint=0003_auth_rls_initplan | {"name": "foo", "type": "table", "schema": "public"} | auth_rls_init_plan_public_foo_bad_policy_role - auth_rls_initplan | Auth RLS Initialization Plan | WARN | EXTERNAL | {PERFORMANCE} | Detects if calls to \`current_setting()\` and \`auth.()\` in RLS policies are being unnecessarily re-evaluated for each row | Table \`public.foo\` has a row level security policy \`bad_policy_email\` that re-evaluates current_setting() or auth.() for each row. This produces suboptimal query performance at scale. Resolve the issue by replacing \`auth.()\` with \`(select auth.())\`. See [docs](https://supabase.com/docs/guides/database/postgres/row-level-security#call-functions-with-select) for more info. | https://supabase.com/docs/guides/database/database-linter?lint=0003_auth_rls_initplan | {"name": "foo", "type": "table", "schema": "public"} | auth_rls_init_plan_public_foo_bad_policy_email - auth_rls_initplan | Auth RLS Initialization Plan | WARN | EXTERNAL | {PERFORMANCE} | Detects if calls to \`current_setting()\` and \`auth.()\` in RLS policies are being unnecessarily re-evaluated for each row | Table \`public.foo\` has a row level security policy \`bad_policy_current_setting\` that re-evaluates current_setting() or auth.() for each row. This produces suboptimal query performance at scale. Resolve the issue by replacing \`auth.()\` with \`(select auth.())\`. See [docs](https://supabase.com/docs/guides/database/postgres/row-level-security#call-functions-with-select) for more info. | https://supabase.com/docs/guides/database/database-linter?lint=0003_auth_rls_initplan | {"name": "foo", "type": "table", "schema": "public"} | auth_rls_init_plan_public_foo_bad_policy_current_setting + auth_rls_initplan | Auth RLS Initialization Plan | WARN | EXTERNAL | {PERFORMANCE} | Detects if calls to \`current_setting()\` and \`auth.()\` in RLS policies are being unnecessarily re-evaluated for each row | Table \`public.foo\` has a row level security policy \`bad_policy_uid\` that re-evaluates current_setting() or auth.() for each row. This produces suboptimal query performance at scale. Resolve the issue by replacing \`auth.()\` with \`(select auth.())\`. See [docs](https://supabase.com/docs/guides/database/postgres/row-level-security#call-functions-with-select) for more info. | https://supabase.com/docs/guides/database/database-linter?lint=0003_auth_rls_initplan | {"name": "foo", "type": "table", "schema": "public"} | auth_rls_init_plan_public_foo_bad_policy_uid (5 rows) rollback to savepoint a; From c35563499ea10ded10e405430936a574234b646a Mon Sep 17 00:00:00 2001 From: avallete Date: Tue, 20 Oct 2026 17:08:56 +0200 Subject: [PATCH 07/13] perf(lints): compare index keys without a text round trip in unindexed_foreign_keys Slice pg_index.indkey as int2[] instead of building string_to_array(indkey::text, ' ')::smallint[] for every index of every table with a foreign key. The slice starts at the int2vector's 0 lower bound and is rebased to 1, so it compares equal to the same leading columns of conkey (checked: the two forms agree for every index). synthetic 2k tables: 15.5 -> 14.0 ms. carbon: 18.4 -> 16.2 ms. Rows identical on both. Co-Authored-By: Claude Opus 5.5 --- lints/0001_unindexed_foreign_keys.sql | 6 ++++-- splinter.sql | 6 ++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/lints/0001_unindexed_foreign_keys.sql b/lints/0001_unindexed_foreign_keys.sql index f0841407..f7ff469b 100644 --- a/lints/0001_unindexed_foreign_keys.sql +++ b/lints/0001_unindexed_foreign_keys.sql @@ -28,7 +28,9 @@ index_ as ( select pi.indrelid as table_oid, indexrelid::regclass as index_, - string_to_array(indkey::text, ' ')::smallint[] as col_attnums + -- int2vector -> int2[] keeps its 0 lower bound; slices are rebased to 1, so they + -- compare equal to the 1-based conkey + pi.indkey::smallint[] as col_attnums from pg_catalog.pg_index pi where @@ -60,7 +62,7 @@ from foreign_keys fk left join index_ idx on fk.table_oid = idx.table_oid - and fk.col_attnums = idx.col_attnums[1:array_length(fk.col_attnums, 1)] + and fk.col_attnums = idx.col_attnums[0:array_length(fk.col_attnums, 1) - 1] left join pg_catalog.pg_depend dep on idx.table_oid = dep.objid and dep.deptype = 'e' diff --git a/splinter.sql b/splinter.sql index f397da71..435b6e2c 100644 --- a/splinter.sql +++ b/splinter.sql @@ -51,7 +51,9 @@ index_ as ( select pi.indrelid as table_oid, indexrelid::regclass as index_, - string_to_array(indkey::text, ' ')::smallint[] as col_attnums + -- int2vector -> int2[] keeps its 0 lower bound slices are rebased to 1, so they + -- compare equal to the 1-based conkey + pi.indkey::smallint[] as col_attnums from pg_catalog.pg_index pi where @@ -83,7 +85,7 @@ from foreign_keys fk left join index_ idx on fk.table_oid = idx.table_oid - and fk.col_attnums = idx.col_attnums[1:array_length(fk.col_attnums, 1)] + and fk.col_attnums = idx.col_attnums[0:array_length(fk.col_attnums, 1) - 1] left join pg_catalog.pg_depend dep on idx.table_oid = dep.objid and dep.deptype = 'e' From 93fea2c7df5fd5faf5069f0df80271c0c19e17fb Mon Sep 17 00:00:00 2001 From: avallete Date: Tue, 20 Oct 2026 17:09:54 +0200 Subject: [PATCH 08/13] perf(lints): check for a primary key with not exists in no_primary_key The left join to pg_index multiplied each table by its index count before the group by collapsed it back, so the pg_depend anti-join ran once per index instead of once per table. A not-exists on a primary index keeps one row per table throughout; the select list only used grouped columns. synthetic 2k tables: 8.9 -> 2.7 ms, 22.9k -> 1.2k buffers. carbon: 2.6 -> 0.7 ms. Rows identical on both. Co-Authored-By: Claude Opus 5.5 --- lints/0004_no_primary_key.sql | 14 ++++++-------- splinter.sql | 14 ++++++-------- 2 files changed, 12 insertions(+), 16 deletions(-) diff --git a/lints/0004_no_primary_key.sql b/lints/0004_no_primary_key.sql index 289dc23e..36a1a75c 100644 --- a/lints/0004_no_primary_key.sql +++ b/lints/0004_no_primary_key.sql @@ -27,8 +27,6 @@ from pg_catalog.pg_class pgc join pg_catalog.pg_namespace pgns on pgns.oid = pgc.relnamespace - left join pg_catalog.pg_index pgi - on pgi.indrelid = pgc.oid left join pg_catalog.pg_depend dep on pgc.oid = dep.objid and dep.deptype = 'e' @@ -39,9 +37,9 @@ where '_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', 'realtime', 'repack', 'storage', 'supabase_functions', 'supabase_migrations', 'tiger', 'topology', 'vault' ) and dep.objid is null -- exclude tables owned by extensions -group by - pgc.oid, - pgns.nspname, - pgc.relname -having - max(coalesce(pgi.indisprimary, false)::int) = 0; + and not exists ( + select 1 + from pg_catalog.pg_index pgi + where pgi.indrelid = pgc.oid + and pgi.indisprimary + ); diff --git a/splinter.sql b/splinter.sql index 435b6e2c..a496184e 100644 --- a/splinter.sql +++ b/splinter.sql @@ -320,8 +320,6 @@ from pg_catalog.pg_class pgc join pg_catalog.pg_namespace pgns on pgns.oid = pgc.relnamespace - left join pg_catalog.pg_index pgi - on pgi.indrelid = pgc.oid left join pg_catalog.pg_depend dep on pgc.oid = dep.objid and dep.deptype = 'e' @@ -332,12 +330,12 @@ where '_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', 'realtime', 'repack', 'storage', 'supabase_functions', 'supabase_migrations', 'tiger', 'topology', 'vault' ) and dep.objid is null -- exclude tables owned by extensions -group by - pgc.oid, - pgns.nspname, - pgc.relname -having - max(coalesce(pgi.indisprimary, false)::int) = 0) + and not exists ( + select 1 + from pg_catalog.pg_index pgi + where pgi.indrelid = pgc.oid + and pgi.indisprimary + )) union all ( select From dd70f9f715f6b648064dc91ce0d21f89a8643a22 Mon Sep 17 00:00:00 2001 From: avallete Date: Tue, 20 Oct 2026 17:17:30 +0200 Subject: [PATCH 09/13] perf(lints): resolve the auth schema once in fkey_to_auth_unique Compare the referenced table's namespace with to_regnamespace('auth') instead of joining pg_namespace and filtering by name. to_regnamespace returns null when the schema is missing, so that case still returns no rows. synthetic 2k tables: 913 -> 466 buffers, 1.4 -> 0.8 ms. Medium synthetic and carbon unchanged. Rows identical on all three. Co-Authored-By: Claude Opus 5.5 --- lints/0021_fkey_to_auth_unique.sql | 4 +--- splinter.sql | 4 +--- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/lints/0021_fkey_to_auth_unique.sql b/lints/0021_fkey_to_auth_unique.sql index dc93aa1e..5f2f07c9 100644 --- a/lints/0021_fkey_to_auth_unique.sql +++ b/lints/0021_fkey_to_auth_unique.sql @@ -33,11 +33,9 @@ from on c_rel.relnamespace = n.oid join pg_catalog.pg_class ref_rel on c.confrelid = ref_rel.oid - join pg_catalog.pg_namespace cn - on ref_rel.relnamespace = cn.oid join pg_catalog.pg_index i on c.conindid = i.indexrelid where c.contype = 'f' - and cn.nspname = 'auth' + and ref_rel.relnamespace = pg_catalog.to_regnamespace('auth') -- null, so no rows, if auth is missing and i.indisunique and not i.indisprimary; diff --git a/splinter.sql b/splinter.sql index a496184e..952ea7be 100644 --- a/splinter.sql +++ b/splinter.sql @@ -1143,12 +1143,10 @@ from on c_rel.relnamespace = n.oid join pg_catalog.pg_class ref_rel on c.confrelid = ref_rel.oid - join pg_catalog.pg_namespace cn - on ref_rel.relnamespace = cn.oid join pg_catalog.pg_index i on c.conindid = i.indexrelid where c.contype = 'f' - and cn.nspname = 'auth' + and ref_rel.relnamespace = pg_catalog.to_regnamespace('auth') -- null, so no rows, if auth is missing and i.indisunique and not i.indisprimary) union all From 161c1f1bfa24fc915e64255375c3975e58fb0976 Mon Sep 17 00:00:00 2001 From: avallete Date: Tue, 20 Oct 2026 17:21:14 +0200 Subject: [PATCH 10/13] perf(lints): count null columns in the same aggregate in table_bloat nullhdr re-read pg_stats in a correlated subquery for every table, keyed on the same schemaname and tablename the outer query groups by. A count(*) filter over the group counts the same rows without the second pass over the pg_stats view. medium synthetic: 5.7k -> 3.1k buffers. synthetic 2k tables: 6.2k -> 3.6k buffers, 6.9 -> 5.7 ms. carbon: 6.2k -> 2.9k buffers. Rows identical on all three. Co-Authored-By: Claude Opus 5.5 --- lints/0020_table_bloat.sql | 10 ++-------- splinter.sql | 10 ++-------- 2 files changed, 4 insertions(+), 16 deletions(-) diff --git a/lints/0020_table_bloat.sql b/lints/0020_table_bloat.sql index 0704a840..88282408 100644 --- a/lints/0020_table_bloat.sql +++ b/lints/0020_table_bloat.sql @@ -21,14 +21,8 @@ bloat_info as ( bs, sum((1 - null_frac) * avg_width) as datawidth, max(null_frac) as maxfracsum, - hdr + ( - select 1 + count(*) / 8 - from pg_stats s2 - where - null_frac <> 0 - and s2.schemaname = s.schemaname - and s2.tablename = s.tablename - ) as nullhdr + -- the same pg_stats rows the correlated subquery used to re-read: this group's + hdr + 1 + count(*) filter (where null_frac <> 0) / 8 as nullhdr from pg_stats s, constants group by 1, 2, 3, 4, 5 ) as foo diff --git a/splinter.sql b/splinter.sql index 952ea7be..e741cd92 100644 --- a/splinter.sql +++ b/splinter.sql @@ -1032,14 +1032,8 @@ bloat_info as ( bs, sum((1 - null_frac) * avg_width) as datawidth, max(null_frac) as maxfracsum, - hdr + ( - select 1 + count(*) / 8 - from pg_stats s2 - where - null_frac <> 0 - and s2.schemaname = s.schemaname - and s2.tablename = s.tablename - ) as nullhdr + -- the same pg_stats rows the correlated subquery used to re-read: this group's + hdr + 1 + count(*) filter (where null_frac <> 0) / 8 as nullhdr from pg_stats s, constants group by 1, 2, 3, 4, 5 ) as foo From 8e826d832d0a6b39f530b11b73f4f6a19e771b49 Mon Sep 17 00:00:00 2001 From: avallete Date: Wed, 21 Oct 2026 17:20:49 +0200 Subject: [PATCH 11/13] Revert "perf(lints): resolve the auth schema once in fkey_to_auth_unique" This reverts commit dd70f9f715f6b648064dc91ce0d21f89a8643a22. --- lints/0021_fkey_to_auth_unique.sql | 4 +++- splinter.sql | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/lints/0021_fkey_to_auth_unique.sql b/lints/0021_fkey_to_auth_unique.sql index 5f2f07c9..dc93aa1e 100644 --- a/lints/0021_fkey_to_auth_unique.sql +++ b/lints/0021_fkey_to_auth_unique.sql @@ -33,9 +33,11 @@ from on c_rel.relnamespace = n.oid join pg_catalog.pg_class ref_rel on c.confrelid = ref_rel.oid + join pg_catalog.pg_namespace cn + on ref_rel.relnamespace = cn.oid join pg_catalog.pg_index i on c.conindid = i.indexrelid where c.contype = 'f' - and ref_rel.relnamespace = pg_catalog.to_regnamespace('auth') -- null, so no rows, if auth is missing + and cn.nspname = 'auth' and i.indisunique and not i.indisprimary; diff --git a/splinter.sql b/splinter.sql index e741cd92..13e7960d 100644 --- a/splinter.sql +++ b/splinter.sql @@ -1137,10 +1137,12 @@ from on c_rel.relnamespace = n.oid join pg_catalog.pg_class ref_rel on c.confrelid = ref_rel.oid + join pg_catalog.pg_namespace cn + on ref_rel.relnamespace = cn.oid join pg_catalog.pg_index i on c.conindid = i.indexrelid where c.contype = 'f' - and ref_rel.relnamespace = pg_catalog.to_regnamespace('auth') -- null, so no rows, if auth is missing + and cn.nspname = 'auth' and i.indisunique and not i.indisprimary) union all From ef79a0c5c8261a9565dff92584c9454def0c50b3 Mon Sep 17 00:00:00 2001 From: avallete Date: Wed, 21 Oct 2026 17:20:50 +0200 Subject: [PATCH 12/13] Revert "perf(lints): inline pg_stat_user_indexes in unused_index" This reverts commit 8c923b09e5168006485c39306021a52bdb46cb34. --- lints/0005_unused_index.sql | 36 ++++++++++++++---------------------- splinter.sql | 36 ++++++++++++++---------------------- 2 files changed, 28 insertions(+), 44 deletions(-) diff --git a/lints/0005_unused_index.sql b/lints/0005_unused_index.sql index bfa8cabc..4d599a89 100644 --- a/lints/0005_unused_index.sql +++ b/lints/0005_unused_index.sql @@ -9,44 +9,36 @@ select 'Detects if an index has never been used and may be a candidate for removal.' as description, format( 'Index \`%s\` on table \`%s.%s\` has not been used', - i.relname, - n.nspname, - c.relname + psui.indexrelname, + psui.schemaname, + psui.relname ) as detail, 'https://supabase.com/docs/guides/database/database-linter?lint=0005_unused_index' as remediation, jsonb_build_object( - 'schema', n.nspname, - 'name', c.relname, + 'schema', psui.schemaname, + 'name', psui.relname, 'type', 'table' ) as metadata, format( 'unused_index_%s_%s_%s', - n.nspname, - c.relname, - i.relname + psui.schemaname, + psui.relname, + psui.indexrelname ) as cache_key from - -- pg_stat_user_indexes, inlined so idx_scan is read only for the candidate indexes - pg_catalog.pg_index pi - join pg_catalog.pg_class i - on i.oid = pi.indexrelid - join pg_catalog.pg_class c - on c.oid = pi.indrelid - and c.relkind in ('r', 't', 'm') - join pg_catalog.pg_namespace n - on n.oid = c.relnamespace + pg_catalog.pg_stat_user_indexes psui + join pg_catalog.pg_index pi + on psui.indexrelid = pi.indexrelid left join pg_catalog.pg_depend dep - on c.oid = dep.objid + on psui.relid = dep.objid and dep.deptype = 'e' and dep.classid = 'pg_catalog.pg_class'::regclass where - pg_catalog.pg_stat_get_numscans(i.oid) = 0 + psui.idx_scan = 0 and not pi.indisunique and not pi.indisprimary and dep.objid is null -- exclude tables owned by extensions - and n.nspname not in ('pg_catalog', 'information_schema') - and n.nspname !~ '^pg_toast' - and n.nspname not in ( + and psui.schemaname not in ( '_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', 'realtime', 'repack', 'storage', 'supabase_functions', 'supabase_migrations', 'tiger', 'topology', 'vault' ); diff --git a/splinter.sql b/splinter.sql index 13e7960d..dc7f14d3 100644 --- a/splinter.sql +++ b/splinter.sql @@ -347,45 +347,37 @@ select 'Detects if an index has never been used and may be a candidate for removal.' as description, format( 'Index \`%s\` on table \`%s.%s\` has not been used', - i.relname, - n.nspname, - c.relname + psui.indexrelname, + psui.schemaname, + psui.relname ) as detail, 'https://supabase.com/docs/guides/database/database-linter?lint=0005_unused_index' as remediation, jsonb_build_object( - 'schema', n.nspname, - 'name', c.relname, + 'schema', psui.schemaname, + 'name', psui.relname, 'type', 'table' ) as metadata, format( 'unused_index_%s_%s_%s', - n.nspname, - c.relname, - i.relname + psui.schemaname, + psui.relname, + psui.indexrelname ) as cache_key from - -- pg_stat_user_indexes, inlined so idx_scan is read only for the candidate indexes - pg_catalog.pg_index pi - join pg_catalog.pg_class i - on i.oid = pi.indexrelid - join pg_catalog.pg_class c - on c.oid = pi.indrelid - and c.relkind in ('r', 't', 'm') - join pg_catalog.pg_namespace n - on n.oid = c.relnamespace + pg_catalog.pg_stat_user_indexes psui + join pg_catalog.pg_index pi + on psui.indexrelid = pi.indexrelid left join pg_catalog.pg_depend dep - on c.oid = dep.objid + on psui.relid = dep.objid and dep.deptype = 'e' and dep.classid = 'pg_catalog.pg_class'::regclass where - pg_catalog.pg_stat_get_numscans(i.oid) = 0 + psui.idx_scan = 0 and not pi.indisunique and not pi.indisprimary and dep.objid is null -- exclude tables owned by extensions - and n.nspname not in ('pg_catalog', 'information_schema') - and n.nspname !~ '^pg_toast' - and n.nspname not in ( + and psui.schemaname not in ( '_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', 'realtime', 'repack', 'storage', 'supabase_functions', 'supabase_migrations', 'tiger', 'topology', 'vault' )) union all From cfd240b6e4c5080aa16afd57e6320040c7606f86 Mon Sep 17 00:00:00 2001 From: avallete Date: Wed, 21 Oct 2026 17:20:52 +0200 Subject: [PATCH 13/13] Revert "perf(lints): compare index keys without a text round trip in unindexed_foreign_keys" This reverts commit c35563499ea10ded10e405430936a574234b646a. --- lints/0001_unindexed_foreign_keys.sql | 6 ++---- splinter.sql | 6 ++---- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/lints/0001_unindexed_foreign_keys.sql b/lints/0001_unindexed_foreign_keys.sql index f7ff469b..f0841407 100644 --- a/lints/0001_unindexed_foreign_keys.sql +++ b/lints/0001_unindexed_foreign_keys.sql @@ -28,9 +28,7 @@ index_ as ( select pi.indrelid as table_oid, indexrelid::regclass as index_, - -- int2vector -> int2[] keeps its 0 lower bound; slices are rebased to 1, so they - -- compare equal to the 1-based conkey - pi.indkey::smallint[] as col_attnums + string_to_array(indkey::text, ' ')::smallint[] as col_attnums from pg_catalog.pg_index pi where @@ -62,7 +60,7 @@ from foreign_keys fk left join index_ idx on fk.table_oid = idx.table_oid - and fk.col_attnums = idx.col_attnums[0:array_length(fk.col_attnums, 1) - 1] + and fk.col_attnums = idx.col_attnums[1:array_length(fk.col_attnums, 1)] left join pg_catalog.pg_depend dep on idx.table_oid = dep.objid and dep.deptype = 'e' diff --git a/splinter.sql b/splinter.sql index dc7f14d3..9a1708a8 100644 --- a/splinter.sql +++ b/splinter.sql @@ -51,9 +51,7 @@ index_ as ( select pi.indrelid as table_oid, indexrelid::regclass as index_, - -- int2vector -> int2[] keeps its 0 lower bound slices are rebased to 1, so they - -- compare equal to the 1-based conkey - pi.indkey::smallint[] as col_attnums + string_to_array(indkey::text, ' ')::smallint[] as col_attnums from pg_catalog.pg_index pi where @@ -85,7 +83,7 @@ from foreign_keys fk left join index_ idx on fk.table_oid = idx.table_oid - and fk.col_attnums = idx.col_attnums[0:array_length(fk.col_attnums, 1) - 1] + and fk.col_attnums = idx.col_attnums[1:array_length(fk.col_attnums, 1)] left join pg_catalog.pg_depend dep on idx.table_oid = dep.objid and dep.deptype = 'e'