Migrations are version-controlled changes to your database schema. This guide walks through creating, testing, and deploying migrations in the FindU ecosystem.
# Create a migrations directory in your repomkdir -p migrations# Create a new migration file with timestamptouch migrations/$(date +%Y%m%d%H%M%S)_add_user_preferences_table.sql
3
Write your migration
Edit the generated file with your SQL:
Copy
-- Create user preferences tableCREATE TABLE IF NOT EXISTS public.user_preferences ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), student_id uuid NOT NULL REFERENCES public.students(id) ON DELETE CASCADE, theme text DEFAULT 'light' CHECK (theme IN ('light', 'dark', 'auto')), notifications_enabled boolean DEFAULT true, created_at timestamptz DEFAULT now(), updated_at timestamptz DEFAULT now());-- Add RLS policiesALTER TABLE public.user_preferences ENABLE ROW LEVEL SECURITY;-- Students can only see their own preferencesCREATE POLICY "Students can view own preferences" ON public.user_preferences FOR SELECT USING (auth.uid() IN ( SELECT user_id FROM public.profiles WHERE student_id = user_preferences.student_id ));-- Students can update their own preferencesCREATE POLICY "Students can update own preferences" ON public.user_preferences FOR UPDATE USING (auth.uid() IN ( SELECT user_id FROM public.profiles WHERE student_id = user_preferences.student_id ));-- Create updated_at triggerCREATE TRIGGER update_user_preferences_updated_at BEFORE UPDATE ON public.user_preferences FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
4
Test your migration
Go to Supabase dashboard
Navigate to SQL Editor
Paste your migration SQL
Run in a transaction:
Copy
BEGIN;-- Your migration SQL here-- If it looks good:COMMIT;-- If something's wrong:-- ROLLBACK;
5
Commit and push
Copy
git add .git commit -m "feat: add user preferences table with RLS"git push origin feature/add-user-preferences
6
Create pull request
Copy
gh pr create --base dev --title "Add user preferences table" \ --body "Adds table for storing user theme and notification preferences"
-- Always use IF NOT EXISTS for safetyCREATE TABLE IF NOT EXISTS public.feature_flags ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), name text NOT NULL UNIQUE, enabled boolean DEFAULT false, rollout_percentage integer DEFAULT 0 CHECK (rollout_percentage BETWEEN 0 AND 100), created_at timestamptz DEFAULT now());-- Always add RLSALTER TABLE public.feature_flags ENABLE ROW LEVEL SECURITY;-- Add appropriate indexesCREATE INDEX idx_feature_flags_name ON public.feature_flags(name);
Data migrations should be idempotent - running them multiple times should have the same result.
Copy
-- Example: Backfill data with proper checksUPDATE public.studentsSET areas_of_interest = ARRAY['undecided']WHERE areas_of_interest IS NULL OR array_length(areas_of_interest, 1) = 0;-- Example: Migrate data between tablesINSERT INTO public.user_preferences (student_id, theme, notifications_enabled)SELECT id as student_id, COALESCE(settings->>'theme', 'light') as theme, COALESCE((settings->>'notifications')::boolean, true) as notifications_enabledFROM public.studentsWHERE NOT EXISTS ( SELECT 1 FROM public.user_preferences WHERE student_id = students.id);
-- Utility function for updated_at timestampsCREATE OR REPLACE FUNCTION update_updated_at_column()RETURNS TRIGGER AS $$BEGIN NEW.updated_at = now(); RETURN NEW;END;$$ LANGUAGE plpgsql;-- Domain-specific functionCREATE OR REPLACE FUNCTION calculate_match_score( student_id uuid, school_id uuid) RETURNS numeric AS $$DECLARE score numeric;BEGIN -- Your matching logic here SELECT CASE WHEN s.gpa >= sch.min_gpa THEN 0.25 ELSE 0 END + -- Add more scoring logic 0.5 -- Base score INTO score FROM public.students s, public.schools sch WHERE s.id = student_id AND sch.id = school_id; RETURN COALESCE(score, 0);END;$$ LANGUAGE plpgsql;
-- Migration: 20240205123456_add_complex_feature.sql-- Rollback: 20240205123457_rollback_complex_feature.sql-- Drop new constraintsALTER TABLE public.students DROP CONSTRAINT IF EXISTS check_gpa;-- Drop new columnsALTER TABLE public.students DROP COLUMN IF EXISTS new_feature;-- Drop new tablesDROP TABLE IF EXISTS public.user_preferences CASCADE;
-- Enable RLSALTER TABLE public.table_name ENABLE ROW LEVEL SECURITY;-- Public read accessCREATE POLICY "Public can view" ON public.table_name FOR SELECT USING (true);-- Authenticated write accessCREATE POLICY "Authenticated can insert" ON public.table_name FOR INSERT WITH CHECK (auth.role() = 'authenticated');-- Owner-only accessCREATE POLICY "Users can manage own records" ON public.table_name FOR ALL USING (auth.uid() = user_id);
-- Foreign key indexes (often missed)CREATE INDEX idx_table_foreign_key ON public.table_name(foreign_key_id);-- Timestamp indexes for queriesCREATE INDEX idx_table_created_at ON public.table_name(created_at DESC);-- Text search indexesCREATE INDEX idx_table_search ON public.table_name USING gin(to_tsvector('english', name || ' ' || description));