update flash card, test

This commit is contained in:
2026-04-15 00:41:02 +07:00
parent 4bc39225ab
commit 088c555515
32 changed files with 1988 additions and 415 deletions

View File

@@ -0,0 +1,302 @@
-- 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()
)
);

View File

@@ -0,0 +1,3 @@
-- Migration 005: Make test_id nullable on user_test_attempt
-- Practice sessions (part-based) don't belong to a specific test record.
ALTER TABLE user_test_attempt ALTER COLUMN test_id DROP NOT NULL;