303 lines
12 KiB
SQL
303 lines
12 KiB
SQL
-- Migration 004: Full schema reset
|
|
-- Drops legacy flat tables (questions, vocab, user_progress)
|
|
-- Creates new hierarchical schema from supabase/create/ files
|
|
-- Adapted for Supabase: users(id) → auth.users(id) UUID
|
|
-- Tables kept intact: writing_submissions, user_gamification, xu_transactions, weekly_leaderboard
|
|
|
|
-- ============================================================
|
|
-- DROP LEGACY TABLES
|
|
-- ============================================================
|
|
DROP TABLE IF EXISTS user_progress CASCADE;
|
|
DROP TABLE IF EXISTS vocab CASCADE;
|
|
DROP TABLE IF EXISTS questions CASCADE;
|
|
|
|
-- ============================================================
|
|
-- TEST STRUCTURE
|
|
-- (from create/test.sql + create/update.sql merged)
|
|
-- ============================================================
|
|
|
|
CREATE TABLE test_category (
|
|
id SERIAL PRIMARY KEY,
|
|
name VARCHAR(100) NOT NULL,
|
|
slug VARCHAR(100) UNIQUE
|
|
);
|
|
|
|
CREATE TABLE test (
|
|
id SERIAL PRIMARY KEY,
|
|
title VARCHAR(255) NOT NULL,
|
|
description TEXT,
|
|
total_questions INT DEFAULT 0,
|
|
duration_minutes INT DEFAULT 120,
|
|
category_id INT REFERENCES test_category(id),
|
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
);
|
|
|
|
CREATE TABLE part (
|
|
id SERIAL PRIMARY KEY,
|
|
test_id INT NOT NULL REFERENCES test(id) ON DELETE CASCADE,
|
|
part_number INT NOT NULL,
|
|
title VARCHAR(100) NOT NULL,
|
|
question_count INT DEFAULT 0,
|
|
display_order INT DEFAULT 0,
|
|
UNIQUE (test_id, part_number)
|
|
);
|
|
|
|
CREATE TABLE tag (
|
|
id SERIAL PRIMARY KEY,
|
|
name VARCHAR(255) NOT NULL UNIQUE
|
|
);
|
|
|
|
CREATE TABLE part_tag (
|
|
part_id INT NOT NULL REFERENCES part(id) ON DELETE CASCADE,
|
|
tag_id INT NOT NULL REFERENCES tag(id) ON DELETE CASCADE,
|
|
PRIMARY KEY (part_id, tag_id)
|
|
);
|
|
|
|
CREATE TABLE question_group (
|
|
id SERIAL PRIMARY KEY,
|
|
part_id INT NOT NULL REFERENCES part(id) ON DELETE CASCADE,
|
|
audio_url VARCHAR(500),
|
|
image_url VARCHAR(500),
|
|
passage_text TEXT,
|
|
display_order INT DEFAULT 0
|
|
);
|
|
|
|
CREATE TABLE question (
|
|
id SERIAL PRIMARY KEY,
|
|
group_id INT NOT NULL REFERENCES question_group(id) ON DELETE CASCADE,
|
|
question_number INT NOT NULL,
|
|
question_text TEXT,
|
|
explanation TEXT,
|
|
display_order INT DEFAULT 0
|
|
);
|
|
|
|
CREATE TABLE answer_choice (
|
|
id SERIAL PRIMARY KEY,
|
|
question_id INT NOT NULL REFERENCES question(id) ON DELETE CASCADE,
|
|
value CHAR(1) NOT NULL CHECK (value IN ('A', 'B', 'C', 'D')),
|
|
label_text TEXT,
|
|
is_correct BOOLEAN NOT NULL DEFAULT FALSE
|
|
);
|
|
|
|
-- ============================================================
|
|
-- FLASHCARD SYSTEM
|
|
-- (from create/flash_card.sql)
|
|
-- adapted: user_id / created_by → auth.users(id) UUID
|
|
-- ============================================================
|
|
|
|
CREATE TABLE flashcard_list (
|
|
id SERIAL PRIMARY KEY,
|
|
title VARCHAR(255) NOT NULL,
|
|
description TEXT,
|
|
total_words INT DEFAULT 0,
|
|
is_public BOOLEAN DEFAULT TRUE,
|
|
created_by UUID REFERENCES auth.users(id) ON DELETE SET NULL,
|
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
);
|
|
|
|
CREATE TABLE flashcard_term (
|
|
id SERIAL PRIMARY KEY,
|
|
list_id INT NOT NULL REFERENCES flashcard_list(id) ON DELETE CASCADE,
|
|
word VARCHAR(255) NOT NULL,
|
|
part_of_speech VARCHAR(50),
|
|
phonetic VARCHAR(100),
|
|
definition TEXT,
|
|
example TEXT,
|
|
image_url VARCHAR(500),
|
|
audio_tts_text VARCHAR(255),
|
|
audio_lang VARCHAR(10) DEFAULT 'en-US',
|
|
display_order INT DEFAULT 0,
|
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
);
|
|
|
|
CREATE TABLE user_flashcard_list (
|
|
user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
|
|
list_id INT NOT NULL REFERENCES flashcard_list(id) ON DELETE CASCADE,
|
|
enrolled_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
PRIMARY KEY (user_id, list_id)
|
|
);
|
|
|
|
CREATE TABLE user_flashcard_progress (
|
|
id SERIAL PRIMARY KEY,
|
|
user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
|
|
term_id INT NOT NULL REFERENCES flashcard_term(id) ON DELETE CASCADE,
|
|
list_id INT NOT NULL REFERENCES flashcard_list(id) ON DELETE CASCADE,
|
|
status VARCHAR(20) NOT NULL DEFAULT 'new', -- new | learning | known | ignored
|
|
ease_factor DECIMAL(4,2) DEFAULT 1.0, -- 1.0=easy | 0.65=medium | 0.1=hard | -1=known/ignored
|
|
review_count INT DEFAULT 0,
|
|
last_reviewed_at TIMESTAMP,
|
|
next_review_at TIMESTAMP,
|
|
UNIQUE (user_id, term_id, list_id)
|
|
);
|
|
|
|
CREATE TABLE user_flashcard_session (
|
|
id SERIAL PRIMARY KEY,
|
|
user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
|
|
list_id INT NOT NULL REFERENCES flashcard_list(id) ON DELETE CASCADE,
|
|
started_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
ended_at TIMESTAMP,
|
|
terms_reviewed INT DEFAULT 0,
|
|
terms_new INT DEFAULT 0
|
|
);
|
|
|
|
CREATE TABLE user_flashcard_review_log (
|
|
id SERIAL PRIMARY KEY,
|
|
session_id INT NOT NULL REFERENCES user_flashcard_session(id) ON DELETE CASCADE,
|
|
term_id INT NOT NULL REFERENCES flashcard_term(id) ON DELETE CASCADE,
|
|
user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
|
|
action_value DECIMAL(4,2) NOT NULL, -- 1 | 0.65 | 0.1 | -1
|
|
reviewed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
);
|
|
|
|
CREATE TABLE user_flashcard_settings (
|
|
user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
|
|
list_id INT NOT NULL REFERENCES flashcard_list(id) ON DELETE CASCADE,
|
|
daily_new_limit INT DEFAULT 20,
|
|
shuffle BOOLEAN DEFAULT TRUE,
|
|
front_side VARCHAR(10) DEFAULT 'word', -- 'word' | 'definition'
|
|
show_all_terms BOOLEAN DEFAULT FALSE,
|
|
PRIMARY KEY (user_id, list_id)
|
|
);
|
|
|
|
-- ============================================================
|
|
-- USER TEST HISTORY
|
|
-- (from create/user_test_history.sql)
|
|
-- adapted: user_id → auth.users(id) UUID
|
|
-- ============================================================
|
|
|
|
CREATE TABLE user_test_attempt (
|
|
id SERIAL PRIMARY KEY,
|
|
user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
|
|
test_id INT NOT NULL REFERENCES test(id) ON DELETE CASCADE,
|
|
selected_parts INT[],
|
|
time_limit_minutes INT,
|
|
started_at TIMESTAMP,
|
|
submitted_at TIMESTAMP,
|
|
time_spent_seconds INT,
|
|
total_correct INT DEFAULT 0,
|
|
total_questions INT DEFAULT 0,
|
|
score DECIMAL(5,2),
|
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
);
|
|
|
|
CREATE TABLE user_answer (
|
|
id SERIAL PRIMARY KEY,
|
|
attempt_id INT NOT NULL REFERENCES user_test_attempt(id) ON DELETE CASCADE,
|
|
question_id INT NOT NULL REFERENCES question(id),
|
|
selected_value CHAR(1),
|
|
is_correct BOOLEAN,
|
|
UNIQUE (attempt_id, question_id)
|
|
);
|
|
|
|
-- ============================================================
|
|
-- INDEXES
|
|
-- ============================================================
|
|
|
|
-- test structure
|
|
CREATE INDEX idx_part_test_id ON part(test_id);
|
|
CREATE INDEX idx_qgroup_part_id ON question_group(part_id);
|
|
CREATE INDEX idx_question_group_id ON question(group_id);
|
|
CREATE INDEX idx_answer_question_id ON answer_choice(question_id);
|
|
CREATE INDEX idx_test_category_id ON test(category_id);
|
|
|
|
-- flashcard
|
|
CREATE INDEX idx_term_list_id ON flashcard_term(list_id);
|
|
CREATE INDEX idx_term_display_order ON flashcard_term(list_id, display_order);
|
|
CREATE INDEX idx_progress_user ON user_flashcard_progress(user_id);
|
|
CREATE INDEX idx_progress_next_review ON user_flashcard_progress(user_id, next_review_at);
|
|
CREATE INDEX idx_progress_status ON user_flashcard_progress(user_id, list_id, status);
|
|
CREATE INDEX idx_review_log_session ON user_flashcard_review_log(session_id);
|
|
CREATE INDEX idx_enrolled_user ON user_flashcard_list(user_id);
|
|
|
|
-- test history
|
|
CREATE INDEX idx_attempt_user_id ON user_test_attempt(user_id);
|
|
CREATE INDEX idx_attempt_test_id ON user_test_attempt(test_id);
|
|
CREATE INDEX idx_answer_attempt_id ON user_answer(attempt_id);
|
|
|
|
-- ============================================================
|
|
-- ROW LEVEL SECURITY
|
|
-- ============================================================
|
|
|
|
-- test content: public read, no direct user writes
|
|
ALTER TABLE test_category ENABLE ROW LEVEL SECURITY;
|
|
ALTER TABLE test ENABLE ROW LEVEL SECURITY;
|
|
ALTER TABLE part ENABLE ROW LEVEL SECURITY;
|
|
ALTER TABLE tag ENABLE ROW LEVEL SECURITY;
|
|
ALTER TABLE part_tag ENABLE ROW LEVEL SECURITY;
|
|
ALTER TABLE question_group ENABLE ROW LEVEL SECURITY;
|
|
ALTER TABLE question ENABLE ROW LEVEL SECURITY;
|
|
ALTER TABLE answer_choice ENABLE ROW LEVEL SECURITY;
|
|
|
|
CREATE POLICY "Public read test_category" ON test_category FOR SELECT USING (true);
|
|
CREATE POLICY "Public read test" ON test FOR SELECT USING (true);
|
|
CREATE POLICY "Public read part" ON part FOR SELECT USING (true);
|
|
CREATE POLICY "Public read tag" ON tag FOR SELECT USING (true);
|
|
CREATE POLICY "Public read part_tag" ON part_tag FOR SELECT USING (true);
|
|
CREATE POLICY "Public read question_group" ON question_group FOR SELECT USING (true);
|
|
CREATE POLICY "Public read question" ON question FOR SELECT USING (true);
|
|
CREATE POLICY "Public read answer_choice" ON answer_choice FOR SELECT USING (true);
|
|
|
|
-- flashcard lists/terms: public lists readable by all, private by owner only
|
|
ALTER TABLE flashcard_list ENABLE ROW LEVEL SECURITY;
|
|
ALTER TABLE flashcard_term ENABLE ROW LEVEL SECURITY;
|
|
|
|
CREATE POLICY "Public read public lists"
|
|
ON flashcard_list FOR SELECT
|
|
USING (is_public = true OR auth.uid() = created_by);
|
|
|
|
CREATE POLICY "Owners can insert lists"
|
|
ON flashcard_list FOR INSERT
|
|
WITH CHECK (auth.uid() = created_by);
|
|
|
|
CREATE POLICY "Public read terms of public lists"
|
|
ON flashcard_term FOR SELECT
|
|
USING (
|
|
EXISTS (
|
|
SELECT 1 FROM flashcard_list fl
|
|
WHERE fl.id = flashcard_term.list_id
|
|
AND (fl.is_public = true OR fl.created_by = auth.uid())
|
|
)
|
|
);
|
|
|
|
-- flashcard user data: users own their rows
|
|
ALTER TABLE user_flashcard_list ENABLE ROW LEVEL SECURITY;
|
|
ALTER TABLE user_flashcard_progress ENABLE ROW LEVEL SECURITY;
|
|
ALTER TABLE user_flashcard_session ENABLE ROW LEVEL SECURITY;
|
|
ALTER TABLE user_flashcard_review_log ENABLE ROW LEVEL SECURITY;
|
|
ALTER TABLE user_flashcard_settings ENABLE ROW LEVEL SECURITY;
|
|
|
|
CREATE POLICY "Users own flashcard_list enrollment" ON user_flashcard_list FOR ALL USING (auth.uid() = user_id) WITH CHECK (auth.uid() = user_id);
|
|
CREATE POLICY "Users own flashcard progress" ON user_flashcard_progress FOR ALL USING (auth.uid() = user_id) WITH CHECK (auth.uid() = user_id);
|
|
CREATE POLICY "Users own flashcard sessions" ON user_flashcard_session FOR ALL USING (auth.uid() = user_id) WITH CHECK (auth.uid() = user_id);
|
|
CREATE POLICY "Users own review logs" ON user_flashcard_review_log FOR ALL USING (auth.uid() = user_id) WITH CHECK (auth.uid() = user_id);
|
|
CREATE POLICY "Users own flashcard settings" ON user_flashcard_settings FOR ALL USING (auth.uid() = user_id) WITH CHECK (auth.uid() = user_id);
|
|
|
|
-- test attempt history: users own their rows
|
|
ALTER TABLE user_test_attempt ENABLE ROW LEVEL SECURITY;
|
|
ALTER TABLE user_answer ENABLE ROW LEVEL SECURITY;
|
|
|
|
CREATE POLICY "Users own test attempts"
|
|
ON user_test_attempt FOR ALL
|
|
USING (auth.uid() = user_id)
|
|
WITH CHECK (auth.uid() = user_id);
|
|
|
|
CREATE POLICY "Users can read own answers"
|
|
ON user_answer FOR SELECT
|
|
USING (
|
|
EXISTS (
|
|
SELECT 1 FROM user_test_attempt uta
|
|
WHERE uta.id = user_answer.attempt_id
|
|
AND uta.user_id = auth.uid()
|
|
)
|
|
);
|
|
|
|
CREATE POLICY "Users can insert own answers"
|
|
ON user_answer FOR INSERT
|
|
WITH CHECK (
|
|
EXISTS (
|
|
SELECT 1 FROM user_test_attempt uta
|
|
WHERE uta.id = user_answer.attempt_id
|
|
AND uta.user_id = auth.uid()
|
|
)
|
|
);
|