From 0d4b69713def3e62bed5b381b015b0336bcfd1da Mon Sep 17 00:00:00 2001 From: denshooter Date: Fri, 24 Apr 2026 12:00:43 +0000 Subject: [PATCH] Fix Supabase linter performance findings - add covering indexes for flagged foreign keys - optimize RLS policies to use select auth.uid() - consolidate posts update policies into one permissive rule - document linter findings and decisions Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- infrastructure/DB_PERFORMANCE_NOTES.md | 64 +++++++++++++++++++ infrastructure/migrations/001_schema.sql | 34 +++++----- .../migrations/002_perf_linter_fixes.sql | 62 ++++++++++++++++++ 3 files changed, 144 insertions(+), 16 deletions(-) create mode 100644 infrastructure/DB_PERFORMANCE_NOTES.md create mode 100644 infrastructure/migrations/002_perf_linter_fixes.sql diff --git a/infrastructure/DB_PERFORMANCE_NOTES.md b/infrastructure/DB_PERFORMANCE_NOTES.md new file mode 100644 index 0000000..d8444a9 --- /dev/null +++ b/infrastructure/DB_PERFORMANCE_NOTES.md @@ -0,0 +1,64 @@ +# Database performance notes (Supabase linter) + +This project had two performance-related linter groups: + +1. **RLS auth function evaluation** +2. **Unindexed foreign keys** + +## What was changed + +1. Added migration `infrastructure/migrations/002_perf_linter_fixes.sql`. +2. Updated `infrastructure/migrations/001_schema.sql` to keep fresh setups aligned with the same fixes. + +### RLS changes + +The affected policies now use: + +- `(select auth.uid())` instead of `auth.uid()` + +This avoids repeated per-row evaluation in RLS plans for these policies: + +- `profiles_update` +- `posts_select` +- `posts_insert` +- `posts_update_own` +- `resonances_insert` +- `resonances_delete` +- `whispers_select` +- `whispers_insert` +- `follows_insert` +- `follows_delete` +- `reports_insert` +- `reports_select_own` + +Also, duplicate permissive update policies on `public.posts` were consolidated by removing `posts_softdelete` and keeping a single `posts_update_own`. + +### FK index changes + +Added covering indexes: + +- `idx_follows_following_id` on `public.follows(following_id)` +- `idx_reports_reporter_id` on `public.reports(reporter_id)` +- `idx_resonances_user_id` on `public.resonances(user_id)` +- `idx_whispers_from_user_id` on `public.whispers(from_user_id)` +- `idx_whispers_post_id` on `public.whispers(post_id)` + +## About "unused index" findings + +`unused_index` is based on `pg_stat_user_indexes.idx_scan`, which can read as zero after restarts or during low traffic. +In this environment, PostgreSQL was recently restarted, so these were not dropped automatically. + +Suggested review query: + +```sql +select + schemaname, + relname as table_name, + indexrelname as index_name, + idx_scan +from pg_stat_user_indexes +where schemaname = 'public' +order by idx_scan asc, indexrelname; +``` + +Drop an index only after observing sustained zero usage over representative production traffic. diff --git a/infrastructure/migrations/001_schema.sql b/infrastructure/migrations/001_schema.sql index 079b730..84867e1 100644 --- a/infrastructure/migrations/001_schema.sql +++ b/infrastructure/migrations/001_schema.sql @@ -87,8 +87,13 @@ CREATE INDEX IF NOT EXISTS idx_posts_created_at ON public.posts(created_at DESC) CREATE INDEX IF NOT EXISTS idx_posts_review ON public.posts(review_status) WHERE review_status != 'ok'; CREATE INDEX IF NOT EXISTS idx_resonances_post ON public.resonances(post_id); +CREATE INDEX IF NOT EXISTS idx_resonances_user_id ON public.resonances(user_id); CREATE INDEX IF NOT EXISTS idx_follows_follower ON public.follows(follower_id); +CREATE INDEX IF NOT EXISTS idx_follows_following_id ON public.follows(following_id); CREATE INDEX IF NOT EXISTS idx_whispers_to ON public.whispers(to_user_id, read_at); +CREATE INDEX IF NOT EXISTS idx_whispers_from_user_id ON public.whispers(from_user_id); +CREATE INDEX IF NOT EXISTS idx_whispers_post_id ON public.whispers(post_id); +CREATE INDEX IF NOT EXISTS idx_reports_reporter_id ON public.reports(reporter_id); -- ── View: Feed Posts ──────────────────────────────────────────────────────── -- Wird in der iOS-App direkt abgefragt — joins alles zusammen @@ -195,7 +200,7 @@ ALTER TABLE public.reports ENABLE ROW LEVEL SECURITY; -- profiles: jeder darf lesen, nur eigenes darf geändert werden CREATE POLICY "profiles_select" ON public.profiles FOR SELECT USING (true); -CREATE POLICY "profiles_update" ON public.profiles FOR UPDATE USING (auth.uid() = id); +CREATE POLICY "profiles_update" ON public.profiles FOR UPDATE USING ((select auth.uid()) = id); -- posts: Feed-Sichtbarkeit (über View geregelt), eigene Verwaltung CREATE POLICY "posts_select" ON public.posts FOR SELECT @@ -204,34 +209,31 @@ CREATE POLICY "posts_select" ON public.posts FOR SELECT AND review_status IN ('ok', 'approved') AND ( NOT is_anonymous - OR user_id = auth.uid() + OR user_id = (select auth.uid()) ) ); CREATE POLICY "posts_insert" ON public.posts FOR INSERT - WITH CHECK (auth.uid() = user_id); + WITH CHECK ((select auth.uid()) = user_id); CREATE POLICY "posts_update_own" ON public.posts FOR UPDATE - USING (auth.uid() = user_id AND deleted_at IS NULL); --- Soft-Delete: nur eigene Posts dürfen als gelöscht markiert werden -CREATE POLICY "posts_softdelete" ON public.posts FOR UPDATE - USING (auth.uid() = user_id) - WITH CHECK (deleted_at IS NOT NULL); + USING ((select auth.uid()) = user_id) + WITH CHECK ((select auth.uid()) = user_id); -- resonances CREATE POLICY "resonances_select" ON public.resonances FOR SELECT USING (true); -CREATE POLICY "resonances_insert" ON public.resonances FOR INSERT WITH CHECK (auth.uid() = user_id); -CREATE POLICY "resonances_delete" ON public.resonances FOR DELETE USING (auth.uid() = user_id); +CREATE POLICY "resonances_insert" ON public.resonances FOR INSERT WITH CHECK ((select auth.uid()) = user_id); +CREATE POLICY "resonances_delete" ON public.resonances FOR DELETE USING ((select auth.uid()) = user_id); -- whispers: nur Absender und Empfänger CREATE POLICY "whispers_select" ON public.whispers FOR SELECT - USING (auth.uid() = from_user_id OR auth.uid() = to_user_id); + USING ((select auth.uid()) = from_user_id OR (select auth.uid()) = to_user_id); CREATE POLICY "whispers_insert" ON public.whispers FOR INSERT - WITH CHECK (auth.uid() = from_user_id); + WITH CHECK ((select auth.uid()) = from_user_id); -- follows CREATE POLICY "follows_select" ON public.follows FOR SELECT USING (true); -CREATE POLICY "follows_insert" ON public.follows FOR INSERT WITH CHECK (auth.uid() = follower_id); -CREATE POLICY "follows_delete" ON public.follows FOR DELETE USING (auth.uid() = follower_id); +CREATE POLICY "follows_insert" ON public.follows FOR INSERT WITH CHECK ((select auth.uid()) = follower_id); +CREATE POLICY "follows_delete" ON public.follows FOR DELETE USING ((select auth.uid()) = follower_id); -- reports: nur eigene einsehen -CREATE POLICY "reports_insert" ON public.reports FOR INSERT WITH CHECK (auth.uid() = reporter_id); -CREATE POLICY "reports_select_own" ON public.reports FOR SELECT USING (auth.uid() = reporter_id); +CREATE POLICY "reports_insert" ON public.reports FOR INSERT WITH CHECK ((select auth.uid()) = reporter_id); +CREATE POLICY "reports_select_own" ON public.reports FOR SELECT USING ((select auth.uid()) = reporter_id); diff --git a/infrastructure/migrations/002_perf_linter_fixes.sql b/infrastructure/migrations/002_perf_linter_fixes.sql new file mode 100644 index 0000000..5ecaf20 --- /dev/null +++ b/infrastructure/migrations/002_perf_linter_fixes.sql @@ -0,0 +1,62 @@ +-- Performance follow-up for DB linter findings: +-- 1) Wrap auth.uid() in RLS policies with SELECT to avoid per-row re-evaluation. +-- 2) Ensure every FK in follows/reports/resonances/whispers has a covering index. +-- 3) Merge duplicate permissive UPDATE policies on posts. + +BEGIN; + +ALTER POLICY profiles_update ON public.profiles + USING ((select auth.uid()) = id); + +ALTER POLICY posts_select ON public.posts + USING ( + deleted_at IS NULL + AND review_status IN ('ok', 'approved') + AND ( + NOT is_anonymous + OR user_id = (select auth.uid()) + ) + ); + +ALTER POLICY posts_insert ON public.posts + WITH CHECK ((select auth.uid()) = user_id); + +DROP POLICY IF EXISTS posts_softdelete ON public.posts; +ALTER POLICY posts_update_own ON public.posts + USING ((select auth.uid()) = user_id) + WITH CHECK ((select auth.uid()) = user_id); + +ALTER POLICY resonances_insert ON public.resonances + WITH CHECK ((select auth.uid()) = user_id); + +ALTER POLICY resonances_delete ON public.resonances + USING ((select auth.uid()) = user_id); + +ALTER POLICY whispers_select ON public.whispers + USING ( + (select auth.uid()) = from_user_id + OR (select auth.uid()) = to_user_id + ); + +ALTER POLICY whispers_insert ON public.whispers + WITH CHECK ((select auth.uid()) = from_user_id); + +ALTER POLICY follows_insert ON public.follows + WITH CHECK ((select auth.uid()) = follower_id); + +ALTER POLICY follows_delete ON public.follows + USING ((select auth.uid()) = follower_id); + +ALTER POLICY reports_insert ON public.reports + WITH CHECK ((select auth.uid()) = reporter_id); + +ALTER POLICY reports_select_own ON public.reports + USING ((select auth.uid()) = reporter_id); + +CREATE INDEX IF NOT EXISTS idx_follows_following_id ON public.follows (following_id); +CREATE INDEX IF NOT EXISTS idx_reports_reporter_id ON public.reports (reporter_id); +CREATE INDEX IF NOT EXISTS idx_resonances_user_id ON public.resonances (user_id); +CREATE INDEX IF NOT EXISTS idx_whispers_from_user_id ON public.whispers (from_user_id); +CREATE INDEX IF NOT EXISTS idx_whispers_post_id ON public.whispers (post_id); + +COMMIT;