nextgenmobile/supabase/schema.sql

407 lines
18 KiB
PL/PgSQL

-- Supabase Schema Initialization for Batsir Productivity Planner
-- Idempotent version - safe to run multiple times without conflicts
-- Enable extensions
CREATE EXTENSION IF NOT EXISTS "pgcrypto";
----------------------------------------------------
-- 1. Tables Setup (Idempotent - IF NOT EXISTS)
----------------------------------------------------
-- Core tables
CREATE TABLE IF NOT EXISTS public.profiles (
id UUID REFERENCES auth.users(id) ON DELETE CASCADE PRIMARY KEY,
email TEXT,
display_name TEXT,
preferences JSONB DEFAULT '{}'::jsonb,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE TABLE IF NOT EXISTS public.habits (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID REFERENCES public.profiles(id) ON DELETE CASCADE NOT NULL,
title TEXT NOT NULL,
frequency TEXT DEFAULT 'daily',
preferred_time TIME,
location TEXT,
two_minute_version TEXT,
anchor_habit_id UUID REFERENCES public.habits(id) ON DELETE SET NULL,
weekend_flexibility BOOLEAN DEFAULT FALSE,
is_active BOOLEAN DEFAULT TRUE,
current_streak INTEGER DEFAULT 0,
max_streak INTEGER DEFAULT 0,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE TABLE IF NOT EXISTS public.schedules (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID REFERENCES public.profiles(id) ON DELETE CASCADE NOT NULL,
date DATE NOT NULL,
time_blocks JSONB DEFAULT '[]'::jsonb,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE TABLE IF NOT EXISTS public.logs (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID REFERENCES public.profiles(id) ON DELETE CASCADE NOT NULL,
habit_id UUID REFERENCES public.habits(id) ON DELETE CASCADE NOT NULL,
status TEXT,
logged_at TIMESTAMPTZ DEFAULT NOW(),
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE TABLE IF NOT EXISTS public.sync_queue (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID REFERENCES public.profiles(id) ON DELETE CASCADE NOT NULL,
table_name TEXT,
operation TEXT,
payload JSONB,
created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE TABLE IF NOT EXISTS public.shortcuts (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID REFERENCES public.profiles(id) ON DELETE CASCADE NOT NULL,
title TEXT,
url TEXT,
icon TEXT,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE TABLE IF NOT EXISTS public.tasks (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID REFERENCES public.profiles(id) ON DELETE CASCADE NOT NULL,
title TEXT,
status TEXT DEFAULT 'todo',
estimated_sessions INTEGER DEFAULT 1,
completed_sessions INTEGER DEFAULT 0,
tag TEXT,
todos JSONB DEFAULT '[]'::jsonb,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE TABLE IF NOT EXISTS public.books (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID REFERENCES public.profiles(id) ON DELETE CASCADE NOT NULL,
title TEXT NOT NULL,
author TEXT,
total_pages INTEGER DEFAULT 0,
current_page INTEGER DEFAULT 0,
last_page_read INTEGER DEFAULT 0,
file_uri TEXT,
cover_uri TEXT,
status TEXT DEFAULT 'want_to_read',
synthesis TEXT,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE TABLE IF NOT EXISTS public.reading_logs (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID REFERENCES public.profiles(id) ON DELETE CASCADE NOT NULL,
book_id UUID REFERENCES public.books(id) ON DELETE CASCADE NOT NULL,
pages_read INTEGER DEFAULT 0,
duration_minutes DOUBLE PRECISION DEFAULT 0,
duration_seconds DOUBLE PRECISION DEFAULT 0,
logged_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE TABLE IF NOT EXISTS public.bookmarks (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID REFERENCES public.profiles(id) ON DELETE CASCADE NOT NULL,
book_id UUID REFERENCES public.books(id) ON DELETE CASCADE NOT NULL,
page_number INTEGER NOT NULL,
note TEXT,
created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE TABLE IF NOT EXISTS public.sync_history (
old_id TEXT NOT NULL,
new_id UUID NOT NULL,
table_name TEXT NOT NULL,
user_id UUID REFERENCES public.profiles(id) ON DELETE CASCADE NOT NULL,
synced_at TIMESTAMPTZ DEFAULT NOW(),
PRIMARY KEY (old_id, table_name, user_id)
);
CREATE TABLE IF NOT EXISTS public.chat_messages (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID REFERENCES public.profiles(id) ON DELETE CASCADE NOT NULL,
text TEXT NOT NULL,
sender TEXT NOT NULL CHECK (sender IN ('user', 'ai')),
created_at TIMESTAMPTZ DEFAULT NOW()
);
----------------------------------------------------
-- 2. Safe Column Migrations (Check before adding)
----------------------------------------------------
DO $$
BEGIN
-- Books table migrations
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'books' AND column_name = 'synthesis') THEN
ALTER TABLE public.books ADD COLUMN synthesis TEXT;
END IF;
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'books' AND column_name = 'last_page_read') THEN
ALTER TABLE public.books ADD COLUMN last_page_read INTEGER DEFAULT 0;
END IF;
-- Reading logs migrations
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'reading_logs' AND column_name = 'duration_seconds') THEN
ALTER TABLE public.reading_logs ADD COLUMN duration_seconds DOUBLE PRECISION DEFAULT 0;
END IF;
-- Alter column type safely (only if needed)
IF EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_name = 'reading_logs' AND column_name = 'duration_minutes' AND data_type = 'integer'
) THEN
ALTER TABLE public.reading_logs ALTER COLUMN duration_minutes TYPE DOUBLE PRECISION;
END IF;
END $$;
----------------------------------------------------
-- 3. RLS Enablement (Idempotent)
----------------------------------------------------
DO $$
DECLARE
tables text[] := ARRAY['profiles', 'habits', 'schedules', 'logs', 'sync_queue', 'shortcuts', 'tasks', 'books', 'reading_logs', 'bookmarks', 'sync_history', 'chat_messages'];
tbl text;
BEGIN
FOREACH tbl IN ARRAY tables
LOOP
EXECUTE format('ALTER TABLE IF EXISTS public.%I ENABLE ROW LEVEL SECURITY', tbl);
END LOOP;
END $$;
----------------------------------------------------
-- 4. Safe Policy Recreation (Drop only existing policies)
----------------------------------------------------
DO $$
DECLARE
pol RECORD;
BEGIN
FOR pol IN (
SELECT policyname, tablename
FROM pg_policies
WHERE schemaname = 'public'
)
LOOP
BEGIN
EXECUTE format('DROP POLICY IF EXISTS %I ON public.%I', pol.policyname, pol.tablename);
EXCEPTION
WHEN OTHERS THEN
RAISE NOTICE 'Could not drop policy % on table %: %', pol.policyname, pol.tablename, SQLERRM;
END;
END LOOP;
END $$;
-- Profiles (Link is 'id' to auth.uid())
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE policyname = 'Profiles: Users view own' AND tablename = 'profiles') THEN
CREATE POLICY "Profiles: Users view own" ON public.profiles FOR SELECT USING (auth.uid() = id);
CREATE POLICY "Profiles: Users update own" ON public.profiles FOR UPDATE USING (auth.uid() = id);
CREATE POLICY "Profiles: Users insert own" ON public.profiles FOR INSERT WITH CHECK (auth.uid() = id);
END IF;
END $$;
-- Repeat for each table (only create if not exists)
DO $$
BEGIN
-- Habits policies
IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE policyname = 'Habits: Users view own' AND tablename = 'habits') THEN
CREATE POLICY "Habits: Users view own" ON public.habits FOR SELECT USING (auth.uid() = user_id);
CREATE POLICY "Habits: Users insert own" ON public.habits FOR INSERT WITH CHECK (auth.uid() = user_id);
CREATE POLICY "Habits: Users update own" ON public.habits FOR UPDATE USING (auth.uid() = user_id);
CREATE POLICY "Habits: Users delete own" ON public.habits FOR DELETE USING (auth.uid() = user_id);
END IF;
-- Schedules policies
IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE policyname = 'Schedules: Users view own' AND tablename = 'schedules') THEN
CREATE POLICY "Schedules: Users view own" ON public.schedules FOR SELECT USING (auth.uid() = user_id);
CREATE POLICY "Schedules: Users insert own" ON public.schedules FOR INSERT WITH CHECK (auth.uid() = user_id);
CREATE POLICY "Schedules: Users update own" ON public.schedules FOR UPDATE USING (auth.uid() = user_id);
CREATE POLICY "Schedules: Users delete own" ON public.schedules FOR DELETE USING (auth.uid() = user_id);
END IF;
-- Logs policies
IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE policyname = 'Logs: Users view own' AND tablename = 'logs') THEN
CREATE POLICY "Logs: Users view own" ON public.logs FOR SELECT USING (auth.uid() = user_id);
CREATE POLICY "Logs: Users insert own" ON public.logs FOR INSERT WITH CHECK (auth.uid() = user_id);
CREATE POLICY "Logs: Users update own" ON public.logs FOR UPDATE USING (auth.uid() = user_id);
CREATE POLICY "Logs: Users delete own" ON public.logs FOR DELETE USING (auth.uid() = user_id);
END IF;
-- Sync Queue policies
IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE policyname = 'SyncQueue: Users view own' AND tablename = 'sync_queue') THEN
CREATE POLICY "SyncQueue: Users view own" ON public.sync_queue FOR SELECT USING (auth.uid() = user_id);
CREATE POLICY "SyncQueue: Users insert own" ON public.sync_queue FOR INSERT WITH CHECK (auth.uid() = user_id);
CREATE POLICY "SyncQueue: Users delete own" ON public.sync_queue FOR DELETE USING (auth.uid() = user_id);
END IF;
-- Shortcuts policies
IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE policyname = 'Shortcuts: Users view own' AND tablename = 'shortcuts') THEN
CREATE POLICY "Shortcuts: Users view own" ON public.shortcuts FOR SELECT USING (auth.uid() = user_id);
CREATE POLICY "Shortcuts: Users insert own" ON public.shortcuts FOR INSERT WITH CHECK (auth.uid() = user_id);
CREATE POLICY "Shortcuts: Users update own" ON public.shortcuts FOR UPDATE USING (auth.uid() = user_id);
CREATE POLICY "Shortcuts: Users delete own" ON public.shortcuts FOR DELETE USING (auth.uid() = user_id);
END IF;
-- Tasks policies
IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE policyname = 'Tasks: Users view own' AND tablename = 'tasks') THEN
CREATE POLICY "Tasks: Users view own" ON public.tasks FOR SELECT USING (auth.uid() = user_id);
CREATE POLICY "Tasks: Users insert own" ON public.tasks FOR INSERT WITH CHECK (auth.uid() = user_id);
CREATE POLICY "Tasks: Users update own" ON public.tasks FOR UPDATE USING (auth.uid() = user_id);
CREATE POLICY "Tasks: Users delete own" ON public.tasks FOR DELETE USING (auth.uid() = user_id);
END IF;
-- Books policies
IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE policyname = 'Books: Users view own' AND tablename = 'books') THEN
CREATE POLICY "Books: Users view own" ON public.books FOR SELECT USING (auth.uid() = user_id);
CREATE POLICY "Books: Users insert own" ON public.books FOR INSERT WITH CHECK (auth.uid() = user_id);
CREATE POLICY "Books: Users update own" ON public.books FOR UPDATE USING (auth.uid() = user_id);
CREATE POLICY "Books: Users delete own" ON public.books FOR DELETE USING (auth.uid() = user_id);
END IF;
-- Reading Logs policies
IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE policyname = 'ReadingLogs: Users view own' AND tablename = 'reading_logs') THEN
CREATE POLICY "ReadingLogs: Users view own" ON public.reading_logs FOR SELECT USING (auth.uid() = user_id);
CREATE POLICY "ReadingLogs: Users insert own" ON public.reading_logs FOR INSERT WITH CHECK (auth.uid() = user_id);
END IF;
-- Bookmarks policies
IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE policyname = 'Bookmarks: Users view own' AND tablename = 'bookmarks') THEN
CREATE POLICY "Bookmarks: Users view own" ON public.bookmarks FOR SELECT USING (auth.uid() = user_id);
CREATE POLICY "Bookmarks: Users insert own" ON public.bookmarks FOR INSERT WITH CHECK (auth.uid() = user_id);
CREATE POLICY "Bookmarks: Users update own" ON public.bookmarks FOR UPDATE USING (auth.uid() = user_id);
CREATE POLICY "Bookmarks: Users delete own" ON public.bookmarks FOR DELETE USING (auth.uid() = user_id);
END IF;
-- Sync History policies
IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE policyname = 'SyncHistory: Users view own' AND tablename = 'sync_history') THEN
CREATE POLICY "SyncHistory: Users view own" ON public.sync_history FOR SELECT USING (auth.uid() = user_id);
CREATE POLICY "SyncHistory: Users insert own" ON public.sync_history FOR INSERT WITH CHECK (auth.uid() = user_id);
END IF;
-- Chat Messages policies
IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE policyname = 'ChatMessages: Users view own' AND tablename = 'chat_messages') THEN
CREATE POLICY "ChatMessages: Users view own" ON public.chat_messages FOR SELECT USING (auth.uid() = user_id);
CREATE POLICY "ChatMessages: Users insert own" ON public.chat_messages FOR INSERT WITH CHECK (auth.uid() = user_id);
END IF;
END $$;
----------------------------------------------------
-- 5. Triggers & Functions (Safe create or replace)
----------------------------------------------------
-- Function for updated_at
CREATE OR REPLACE FUNCTION update_updated_at_column()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = NOW();
RETURN NEW;
END;
$$ language 'plpgsql';
-- Profile Handler
CREATE OR REPLACE FUNCTION public.handle_new_user()
RETURNS TRIGGER AS $$
BEGIN
INSERT INTO public.profiles (id, email)
VALUES (new.id, new.email)
ON CONFLICT (id) DO UPDATE
SET email = EXCLUDED.email;
RETURN NEW;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
-- Drop and recreate triggers safely
DO $$
DECLARE
trigger_config record;
BEGIN
-- Drop existing triggers
FOR trigger_config IN (
SELECT tgname AS trigger_name, relname AS table_name
FROM pg_trigger
JOIN pg_class ON pg_trigger.tgrelid = pg_class.oid
WHERE tgname IN ('update_profiles_updated_at', 'update_habits_updated_at', 'update_schedules_updated_at',
'update_logs_updated_at', 'update_shortcuts_updated_at', 'update_tasks_updated_at',
'update_books_updated_at', 'on_auth_user_created')
) LOOP
BEGIN
EXECUTE format('DROP TRIGGER IF EXISTS %I ON public.%I', trigger_config.trigger_name, trigger_config.table_name);
EXCEPTION
WHEN OTHERS THEN
RAISE NOTICE 'Could not drop trigger %: %', trigger_config.trigger_name, SQLERRM;
END;
END LOOP;
END $$;
-- Recreate triggers
CREATE TRIGGER update_profiles_updated_at BEFORE UPDATE ON public.profiles FOR EACH ROW EXECUTE PROCEDURE update_updated_at_column();
CREATE TRIGGER update_habits_updated_at BEFORE UPDATE ON public.habits FOR EACH ROW EXECUTE PROCEDURE update_updated_at_column();
CREATE TRIGGER update_schedules_updated_at BEFORE UPDATE ON public.schedules FOR EACH ROW EXECUTE PROCEDURE update_updated_at_column();
CREATE TRIGGER update_logs_updated_at BEFORE UPDATE ON public.logs FOR EACH ROW EXECUTE PROCEDURE update_updated_at_column();
CREATE TRIGGER update_shortcuts_updated_at BEFORE UPDATE ON public.shortcuts FOR EACH ROW EXECUTE PROCEDURE update_updated_at_column();
CREATE TRIGGER update_tasks_updated_at BEFORE UPDATE ON public.tasks FOR EACH ROW EXECUTE PROCEDURE update_updated_at_column();
CREATE TRIGGER update_books_updated_at BEFORE UPDATE ON public.books FOR EACH ROW EXECUTE PROCEDURE update_updated_at_column();
-- Handle auth trigger separately to avoid duplicate
DROP TRIGGER IF EXISTS on_auth_user_created ON auth.users;
CREATE TRIGGER on_auth_user_created AFTER INSERT ON auth.users FOR EACH ROW EXECUTE PROCEDURE public.handle_new_user();
----------------------------------------------------
-- 6. Storage Setup (Idempotent)
----------------------------------------------------
-- Insert bucket if not exists
INSERT INTO storage.buckets (id, name, public)
VALUES ('books', 'books', false)
ON CONFLICT (id) DO NOTHING;
-- Create storage policies safely
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE policyname = 'Books Bucket: Users upload own' AND schemaname = 'storage') THEN
CREATE POLICY "Books Bucket: Users upload own" ON storage.objects
FOR INSERT WITH CHECK (bucket_id = 'books' AND auth.uid() = owner);
CREATE POLICY "Books Bucket: Users view own" ON storage.objects
FOR SELECT USING (bucket_id = 'books' AND auth.uid() = owner);
CREATE POLICY "Books Bucket: Users delete own" ON storage.objects
FOR DELETE USING (bucket_id = 'books' AND auth.uid() = owner);
END IF;
END $$;
----------------------------------------------------
-- 7. Additional Conflict Prevention
----------------------------------------------------
-- Create indexes for better performance and conflict prevention
CREATE INDEX IF NOT EXISTS idx_habits_user_id ON public.habits(user_id);
CREATE INDEX IF NOT EXISTS idx_schedules_user_date ON public.schedules(user_id, date);
CREATE INDEX IF NOT EXISTS idx_logs_user_habit ON public.logs(user_id, habit_id);
CREATE INDEX IF NOT EXISTS idx_tasks_user_id ON public.tasks(user_id);
CREATE INDEX IF NOT EXISTS idx_books_user_id ON public.books(user_id);
CREATE INDEX IF NOT EXISTS idx_reading_logs_book_id ON public.reading_logs(book_id);
CREATE INDEX IF NOT EXISTS idx_bookmarks_book_id ON public.bookmarks(book_id);
-- Add comments for documentation
COMMENT ON TABLE public.profiles IS 'User profiles with preferences';
COMMENT ON TABLE public.habits IS 'User habits with streak tracking';
COMMENT ON TABLE public.schedules IS 'Daily schedules with time blocks';
COMMENT ON TABLE public.logs IS 'Habit completion logs';
COMMENT ON TABLE public.tasks IS 'Task management';
COMMENT ON TABLE public.books IS 'Book library with reading progress';