import asyncpg import logging from config import DB_CONFIG logger = logging.getLogger(__name__) db_pool = None async def get_pool(): global db_pool if db_pool is None: db_pool = await asyncpg.create_pool(**DB_CONFIG, min_size=2, max_size=10) return db_pool async def init_db(): pool = await get_pool() async with pool.acquire() as conn: # ============ users ============ await conn.execute(""" CREATE TABLE IF NOT EXISTS users ( id SERIAL PRIMARY KEY, telegram_id BIGINT UNIQUE NOT NULL, username TEXT, app_language TEXT DEFAULT 'pl', current_level TEXT DEFAULT 'A1', last_completed_level INTEGER DEFAULT 0, created_at TIMESTAMP DEFAULT NOW() ) """) # ============ Авто-миграция: добавляем current_level в users ============ col_exists = await conn.fetchval(""" SELECT 1 FROM information_schema.columns WHERE table_name = 'users' AND column_name = 'current_level' """) if not col_exists: await conn.execute(""" ALTER TABLE users ADD COLUMN current_level TEXT DEFAULT 'A1' """) logger.info("Added current_level column to users") # ============ levels ============ await conn.execute(""" CREATE TABLE IF NOT EXISTS levels ( id SERIAL PRIMARY KEY, level_number INTEGER NOT NULL, grid_size INTEGER NOT NULL, game_type TEXT DEFAULT 'wordsearch', level TEXT DEFAULT 'A1', grid_letters JSONB NOT NULL DEFAULT '[]', word_paths JSONB NOT NULL DEFAULT '{}' ) """) # ============ Убираем старый UNIQUE constraint с levels.level_number ============ await conn.execute(""" ALTER TABLE levels DROP CONSTRAINT IF EXISTS levels_level_number_key """) # ============ Добавляем составной UNIQUE (level, level_number) ============ await conn.execute(""" DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_constraint WHERE conname = 'levels_level_number_unique' ) THEN ALTER TABLE levels ADD CONSTRAINT levels_level_number_unique UNIQUE (level, level_number); END IF; END $$; """) # ============ level_words ============ await conn.execute(""" CREATE TABLE IF NOT EXISTS level_words ( id SERIAL PRIMARY KEY, level_id INTEGER REFERENCES levels(id) ON DELETE CASCADE, word_pl TEXT NOT NULL, translation_ru TEXT DEFAULT '', translation_uk TEXT DEFAULT '', translation_en TEXT DEFAULT '', level TEXT DEFAULT 'A1' ) """) # ============ Авто-миграция: добавляем level в level_words ============ col_exists = await conn.fetchval(""" SELECT 1 FROM information_schema.columns WHERE table_name = 'level_words' AND column_name = 'level' """) if not col_exists: await conn.execute(""" ALTER TABLE level_words ADD COLUMN level TEXT DEFAULT 'A1' """) logger.info("Added level column to level_words") # ============ user_progress ============ existing = await conn.fetchrow(""" SELECT data_type FROM information_schema.columns WHERE table_name = 'user_progress' AND column_name = 'user_id' """) if existing and existing['data_type'] != 'bigint': logger.info("Migrating user_progress.user_id from INTEGER to BIGINT") await conn.execute(""" ALTER TABLE user_progress DROP CONSTRAINT IF EXISTS user_progress_pkey CASCADE """) await conn.execute(""" ALTER TABLE user_progress DROP CONSTRAINT IF EXISTS user_progress_user_id_fkey """) await conn.execute(""" ALTER TABLE user_progress ALTER COLUMN user_id TYPE BIGINT """) await conn.execute(""" ALTER TABLE user_progress ADD PRIMARY KEY (user_id, level_id) """) await conn.execute(""" ALTER TABLE user_progress ADD FOREIGN KEY (user_id) REFERENCES users(telegram_id) ON DELETE CASCADE """) elif not existing: await conn.execute(""" CREATE TABLE IF NOT EXISTS user_progress ( user_id BIGINT REFERENCES users(telegram_id) ON DELETE CASCADE, level_id INTEGER REFERENCES levels(id) ON DELETE CASCADE, completed_at TIMESTAMP, hints_used INTEGER DEFAULT 0, score INTEGER DEFAULT 0, level TEXT DEFAULT 'A1', PRIMARY KEY (user_id, level_id) ) """) # ============ Авто-миграция: добавляем level в user_progress ============ col_exists = await conn.fetchval(""" SELECT 1 FROM information_schema.columns WHERE table_name = 'user_progress' AND column_name = 'level' """) if not col_exists: await conn.execute(""" ALTER TABLE user_progress ADD COLUMN level TEXT DEFAULT 'A1' """) logger.info("Added level column to user_progress") # ============ sentences ============ await conn.execute(""" CREATE TABLE IF NOT EXISTS sentences ( id SERIAL PRIMARY KEY, sentence_number INTEGER NOT NULL, sentence_pl TEXT NOT NULL, translation_ru TEXT DEFAULT '', translation_uk TEXT DEFAULT '', translation_en TEXT DEFAULT '', difficulty INTEGER DEFAULT 1, level TEXT DEFAULT 'A1' ) """) # ============ Убираем старый UNIQUE constraint с sentences.sentence_number ============ await conn.execute(""" ALTER TABLE sentences DROP CONSTRAINT IF EXISTS sentences_sentence_number_key """) # ============ Добавляем составной UNIQUE (level, sentence_number) ============ await conn.execute(""" DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_constraint WHERE conname = 'sentences_sentence_number_unique' ) THEN ALTER TABLE sentences ADD CONSTRAINT sentences_sentence_number_unique UNIQUE (level, sentence_number); END IF; END $$; """) # ============ sentence_progress ============ await conn.execute(""" CREATE TABLE IF NOT EXISTS sentence_progress ( user_id BIGINT REFERENCES users(telegram_id) ON DELETE CASCADE, sentence_id INTEGER REFERENCES sentences(id) ON DELETE CASCADE, completed_at TIMESTAMP, score INTEGER DEFAULT 0, level TEXT DEFAULT 'A1', PRIMARY KEY (user_id, sentence_id) ) """) # ============ Авто-миграция: добавляем level в sentence_progress ============ col_exists = await conn.fetchval(""" SELECT 1 FROM information_schema.columns WHERE table_name = 'sentence_progress' AND column_name = 'level' """) if not col_exists: await conn.execute(""" ALTER TABLE sentence_progress ADD COLUMN level TEXT DEFAULT 'A1' """) logger.info("Added level column to sentence_progress") # ============ typing_progress ============ await conn.execute(""" CREATE TABLE IF NOT EXISTS typing_progress ( user_id BIGINT REFERENCES users(telegram_id) ON DELETE CASCADE, sentence_id INTEGER REFERENCES sentences(id) ON DELETE CASCADE, completed_at TIMESTAMP DEFAULT NOW(), PRIMARY KEY (user_id, sentence_id) ) """) # ============ Индекс для typing_progress ============ await conn.execute("CREATE INDEX IF NOT EXISTS idx_typing_progress_user ON typing_progress(user_id)") # ============ chat_history ============ await conn.execute(""" CREATE TABLE IF NOT EXISTS chat_history ( id SERIAL PRIMARY KEY, user_id BIGINT REFERENCES users(telegram_id) ON DELETE CASCADE, message TEXT NOT NULL, is_user BOOLEAN DEFAULT TRUE, created_at TIMESTAMP DEFAULT NOW() ) """) # ============ Индекс для chat_history ============ await conn.execute("CREATE INDEX IF NOT EXISTS idx_chat_history_user ON chat_history(user_id, created_at DESC)") # ============ chat_usage ============ await conn.execute(""" CREATE TABLE IF NOT EXISTS chat_usage ( id SERIAL PRIMARY KEY, user_id BIGINT REFERENCES users(telegram_id) ON DELETE CASCADE, usage_date DATE DEFAULT CURRENT_DATE, message_count INTEGER DEFAULT 0, UNIQUE (user_id, usage_date) ) """) # ============ Индекс для chat_usage ============ await conn.execute("CREATE INDEX IF NOT EXISTS idx_chat_usage_user ON chat_usage(user_id, usage_date DESC)") # ============ learning_levels ============ await conn.execute(""" CREATE TABLE IF NOT EXISTS learning_levels ( id SERIAL PRIMARY KEY, code VARCHAR(10) UNIQUE NOT NULL, name_pl TEXT NOT NULL, name_ru TEXT NOT NULL, name_uk TEXT NOT NULL, name_en TEXT NOT NULL, sort_order INT NOT NULL, is_active BOOLEAN DEFAULT TRUE ) """) # ============ learning_topics ============ await conn.execute(""" CREATE TABLE IF NOT EXISTS learning_topics ( id SERIAL PRIMARY KEY, topic_id VARCHAR(100) UNIQUE NOT NULL, level_code VARCHAR(10) NOT NULL, order_number INT NOT NULL, name_pl TEXT NOT NULL, name_ru TEXT NOT NULL, name_uk TEXT NOT NULL, name_en TEXT NOT NULL, description_pl TEXT, description_ru TEXT, description_uk TEXT, description_en TEXT, is_active BOOLEAN DEFAULT TRUE, UNIQUE(level_code, order_number) ) """) # ============ learning_lessons ============ await conn.execute(""" CREATE TABLE IF NOT EXISTS learning_lessons ( id SERIAL PRIMARY KEY, lesson_id VARCHAR(120) UNIQUE NOT NULL, topic_id VARCHAR(100) NOT NULL REFERENCES learning_topics(topic_id), order_number INT NOT NULL, skill_tag VARCHAR(100) NOT NULL, title_pl TEXT NOT NULL, title_ru TEXT NOT NULL, title_uk TEXT NOT NULL, title_en TEXT NOT NULL, objective_pl TEXT, objective_ru TEXT, objective_uk TEXT, objective_en TEXT, explanation_pl TEXT, explanation_ru TEXT, explanation_uk TEXT, explanation_en TEXT, is_active BOOLEAN DEFAULT TRUE, UNIQUE(topic_id, order_number) ) """) # ============ lesson_examples ============ await conn.execute(""" CREATE TABLE IF NOT EXISTS lesson_examples ( id SERIAL PRIMARY KEY, lesson_id VARCHAR(120) NOT NULL REFERENCES learning_lessons(lesson_id), order_number INT NOT NULL, text_pl TEXT NOT NULL, text_ru TEXT, text_uk TEXT, text_en TEXT, audio_url TEXT ) """) # ============ learning_questions ============ await conn.execute(""" CREATE TABLE IF NOT EXISTS learning_questions ( id SERIAL PRIMARY KEY, question_id VARCHAR(150) UNIQUE NOT NULL, lesson_id VARCHAR(120) REFERENCES learning_lessons(lesson_id), topic_id VARCHAR(100) NOT NULL REFERENCES learning_topics(topic_id), context_type VARCHAR(50) NOT NULL, question_type VARCHAR(50) NOT NULL, skill_tag VARCHAR(100) NOT NULL, difficulty INT NOT NULL CHECK (difficulty BETWEEN 1 AND 3), prompt_pl TEXT, prompt_ru TEXT, prompt_uk TEXT, prompt_en TEXT, correct_answer TEXT, hint_type VARCHAR(50), hint_value TEXT, is_test_question BOOLEAN DEFAULT FALSE, test_order INT, options_language TEXT DEFAULT 'pl' ) """) # ============ Авто-миграция: добавляем options_language ============ col_exists = await conn.fetchval(""" SELECT 1 FROM information_schema.columns WHERE table_name = 'learning_questions' AND column_name = 'options_language' """) if not col_exists: await conn.execute(""" ALTER TABLE learning_questions ADD COLUMN options_language TEXT DEFAULT 'pl' """) logger.info("Added options_language column to learning_questions") # ============ question_options ============ await conn.execute(""" CREATE TABLE IF NOT EXISTS question_options ( id SERIAL PRIMARY KEY, question_id VARCHAR(150) NOT NULL REFERENCES learning_questions(question_id), option_key VARCHAR(20) NOT NULL, text_pl TEXT, text_ru TEXT, text_uk TEXT, text_en TEXT, is_correct BOOLEAN NOT NULL, wrong_because VARCHAR(100), UNIQUE(question_id, option_key) ) """) # ============ learning_sessions ============ await conn.execute(""" CREATE TABLE IF NOT EXISTS learning_sessions ( id BIGSERIAL PRIMARY KEY, user_id BIGINT NOT NULL, level_code VARCHAR(10) NOT NULL, topic_id VARCHAR(100) NOT NULL, session_type VARCHAR(30) NOT NULL, started_at TIMESTAMP NOT NULL DEFAULT NOW(), completed_at TIMESTAMP, is_completed BOOLEAN DEFAULT FALSE ) """) # ============ user_learning_answers ============ await conn.execute(""" CREATE TABLE IF NOT EXISTS user_learning_answers ( id BIGSERIAL PRIMARY KEY, user_id BIGINT NOT NULL, session_id BIGINT NOT NULL REFERENCES learning_sessions(id), level_code VARCHAR(10) NOT NULL, topic_id VARCHAR(100) NOT NULL, lesson_id VARCHAR(120), question_id VARCHAR(150) NOT NULL, selected_answer TEXT, correct BOOLEAN, skill_tag VARCHAR(100) NOT NULL, question_type VARCHAR(50) NOT NULL, context_type VARCHAR(50) NOT NULL, difficulty INT NOT NULL, time_taken_ms INT, attempt_number INT DEFAULT 1, hint_used BOOLEAN DEFAULT FALSE, wrong_because VARCHAR(100), is_final_answer BOOLEAN DEFAULT TRUE, created_at TIMESTAMP DEFAULT NOW() ) """) # ============ user_lesson_progress ============ await conn.execute(""" CREATE TABLE IF NOT EXISTS user_lesson_progress ( id BIGSERIAL PRIMARY KEY, user_id BIGINT NOT NULL, lesson_id VARCHAR(120) NOT NULL, started_at TIMESTAMP, completed_at TIMESTAMP, attempts_count INT DEFAULT 0, correct_count INT DEFAULT 0, best_score NUMERIC(5,2), average_time_ms INT, completed BOOLEAN DEFAULT FALSE, UNIQUE(user_id, lesson_id) ) """) # ============ user_topic_progress ============ await conn.execute(""" CREATE TABLE IF NOT EXISTS user_topic_progress ( id BIGSERIAL PRIMARY KEY, user_id BIGINT NOT NULL, topic_id VARCHAR(100) NOT NULL, lessons_total INT DEFAULT 0, lessons_completed INT DEFAULT 0, test_started BOOLEAN DEFAULT FALSE, test_completed BOOLEAN DEFAULT FALSE, test_score NUMERIC(5,2), topic_mastery NUMERIC(5,2), confidence NUMERIC(5,2), completed_at TIMESTAMP, UNIQUE(user_id, topic_id) ) """) # ============ user_skill_mastery (Analytics 2.0 + Этап 2) ============ await conn.execute(""" CREATE TABLE IF NOT EXISTS user_skill_mastery ( id BIGSERIAL PRIMARY KEY, user_id BIGINT NOT NULL, skill_tag VARCHAR(100) NOT NULL, attempts_count INT DEFAULT 0, correct_count INT DEFAULT 0, recognition_attempts INT DEFAULT 0, recognition_correct INT DEFAULT 0, recognition_score NUMERIC(5,2), production_attempts INT DEFAULT 0, production_correct INT DEFAULT 0, production_score NUMERIC(5,2), application_attempts INT DEFAULT 0, application_correct INT DEFAULT 0, application_score NUMERIC(5,2), retention_score NUMERIC(5,2) DEFAULT 0, hint_dependency NUMERIC(5,2) DEFAULT 0, average_time_ms INT DEFAULT 0, median_time_ms INT DEFAULT 0, median_correct_time_ms INT DEFAULT 0, median_wrong_time_ms INT DEFAULT 0, recent_accuracy NUMERIC(5,2) DEFAULT 0, last_3_accuracy NUMERIC(5,2), last_5_accuracy NUMERIC(5,2), last_10_accuracy NUMERIC(5,2), long_term_accuracy NUMERIC(5,2) DEFAULT 0, mastery_score NUMERIC(5,2) DEFAULT 0, previous_mastery_score NUMERIC(5,2) DEFAULT 0, mastery_delta NUMERIC(5,2) DEFAULT 0, trend TEXT DEFAULT 'insufficient_data', retention_status TEXT DEFAULT 'fresh', fluency_score NUMERIC(5,2) DEFAULT 0, passive_knowledge_score NUMERIC(5,2) DEFAULT 0, active_knowledge_score NUMERIC(5,2) DEFAULT 0, confidence NUMERIC(5,2) DEFAULT 0, error_count INT DEFAULT 0, repeated_error_count INT DEFAULT 0, days_since_last_attempt INT DEFAULT 0, last_attempt_at TIMESTAMP, UNIQUE(user_id, skill_tag) ) """) # ============ Авто-миграции для user_skill_mastery (Этап 2) ============ stage2_columns = { "previous_mastery_score": "NUMERIC(5,2) DEFAULT 0", "mastery_delta": "NUMERIC(5,2) DEFAULT 0", "trend": "TEXT DEFAULT 'insufficient_data'", "retention_status": "TEXT DEFAULT 'fresh'", "fluency_score": "NUMERIC(5,2) DEFAULT 0", "passive_knowledge_score": "NUMERIC(5,2) DEFAULT 0", "active_knowledge_score": "NUMERIC(5,2) DEFAULT 0", } for col_name, col_type in stage2_columns.items(): col_exists = await conn.fetchval(""" SELECT 1 FROM information_schema.columns WHERE table_name = 'user_skill_mastery' AND column_name = $1 """, col_name) if not col_exists: await conn.execute(f""" ALTER TABLE user_skill_mastery ADD COLUMN {col_name} {col_type} """) logger.info(f"Added {col_name} column to user_skill_mastery") # ============ ai_user_context ============ await conn.execute(""" CREATE TABLE IF NOT EXISTS ai_user_context ( user_id BIGINT PRIMARY KEY, current_level VARCHAR(10), completed_topics INT DEFAULT 0, level_topics_total INT DEFAULT 0, level_completion_percent NUMERIC(5,2) DEFAULT 0, completed_lessons INT DEFAULT 0, overall_mastery NUMERIC(5,2) DEFAULT 0, strong_skills JSONB DEFAULT '[]', weak_skills JSONB DEFAULT '[]', uncertain_skills JSONB DEFAULT '[]', recurring_errors JSONB DEFAULT '[]', recommended_topics JSONB DEFAULT '[]', recommended_lessons JSONB DEFAULT '[]', last_analysis_at TIMESTAMP, analytics_enabled BOOLEAN DEFAULT FALSE ) """) # ============ Авто-миграция: добавляем level_topics_total ============ col_exists = await conn.fetchval(""" SELECT 1 FROM information_schema.columns WHERE table_name = 'ai_user_context' AND column_name = 'level_topics_total' """) if not col_exists: await conn.execute(""" ALTER TABLE ai_user_context ADD COLUMN level_topics_total INT DEFAULT 0 """) logger.info("Added level_topics_total column to ai_user_context") # ============ Авто-миграция: добавляем level_completion_percent ============ col_exists = await conn.fetchval(""" SELECT 1 FROM information_schema.columns WHERE table_name = 'ai_user_context' AND column_name = 'level_completion_percent' """) if not col_exists: await conn.execute(""" ALTER TABLE ai_user_context ADD COLUMN level_completion_percent NUMERIC(5,2) DEFAULT 0 """) logger.info("Added level_completion_percent column to ai_user_context") # ============ achievements ============ await conn.execute(""" CREATE TABLE IF NOT EXISTS achievements ( id SERIAL PRIMARY KEY, code TEXT UNIQUE NOT NULL, category TEXT DEFAULT 'general', name_pl TEXT NOT NULL, name_ru TEXT NOT NULL, name_uk TEXT NOT NULL, name_en TEXT NOT NULL, description_pl TEXT DEFAULT '', description_ru TEXT DEFAULT '', description_uk TEXT DEFAULT '', description_en TEXT DEFAULT '', icon TEXT DEFAULT '🏆', requirement_type TEXT NOT NULL, requirement_value INTEGER DEFAULT 0 ) """) # ============ user_achievements ============ await conn.execute(""" CREATE TABLE IF NOT EXISTS user_achievements ( user_id BIGINT REFERENCES users(telegram_id) ON DELETE CASCADE, achievement_id INTEGER REFERENCES achievements(id) ON DELETE CASCADE, unlocked_at TIMESTAMP DEFAULT NOW(), PRIMARY KEY (user_id, achievement_id) ) """) # ============ daily_logins ============ await conn.execute(""" CREATE TABLE IF NOT EXISTS daily_logins ( user_id BIGINT REFERENCES users(telegram_id) ON DELETE CASCADE, login_date DATE DEFAULT CURRENT_DATE, PRIMARY KEY (user_id, login_date) ) """) # ============ word_quiz_progress ============ await conn.execute(""" CREATE TABLE IF NOT EXISTS word_quiz_progress ( user_id BIGINT REFERENCES users(telegram_id) ON DELETE CASCADE, words_guessed INTEGER DEFAULT 0, total_score INTEGER DEFAULT 0, PRIMARY KEY (user_id) ) """) # ============ Авто-миграция: добавляем correct_streak ============ col_exists = await conn.fetchval(""" SELECT 1 FROM information_schema.columns WHERE table_name = 'word_quiz_progress' AND column_name = 'correct_streak' """) if not col_exists: await conn.execute(""" ALTER TABLE word_quiz_progress ADD COLUMN correct_streak INTEGER DEFAULT 0 """) logger.info("Added correct_streak column to word_quiz_progress") # ============ Авто-миграция: добавляем recent_words ============ col_exists = await conn.fetchval(""" SELECT 1 FROM information_schema.columns WHERE table_name = 'word_quiz_progress' AND column_name = 'recent_words' """) if not col_exists: await conn.execute(""" ALTER TABLE word_quiz_progress ADD COLUMN recent_words TEXT[] DEFAULT '{}' """) logger.info("Added recent_words column to word_quiz_progress") # ============ Авто-миграция: добавляем level в word_quiz_progress ============ col_exists = await conn.fetchval(""" SELECT 1 FROM information_schema.columns WHERE table_name = 'word_quiz_progress' AND column_name = 'level' """) if not col_exists: await conn.execute(""" ALTER TABLE word_quiz_progress ADD COLUMN level TEXT DEFAULT 'A1' """) logger.info("Added level column to word_quiz_progress") # ============ bubble_words_progress ============ await conn.execute(""" CREATE TABLE IF NOT EXISTS bubble_words_progress ( user_id BIGINT REFERENCES users(telegram_id) ON DELETE CASCADE, levels_completed INTEGER DEFAULT 0, total_score INTEGER DEFAULT 0, PRIMARY KEY (user_id) ) """) # ============ Авто-миграция: добавляем level в bubble_words_progress ============ col_exists = await conn.fetchval(""" SELECT 1 FROM information_schema.columns WHERE table_name = 'bubble_words_progress' AND column_name = 'level' """) if not col_exists: await conn.execute(""" ALTER TABLE bubble_words_progress ADD COLUMN level TEXT DEFAULT 'A1' """) logger.info("Added level column to bubble_words_progress") # ============ wrong_because_explanations ============ await conn.execute(""" CREATE TABLE IF NOT EXISTS wrong_because_explanations ( id SERIAL PRIMARY KEY, wrong_because VARCHAR(100) UNIQUE NOT NULL, explanation_pl TEXT NOT NULL, explanation_ru TEXT NOT NULL, explanation_uk TEXT NOT NULL, explanation_en TEXT NOT NULL ) """) # ============ teacher_reactions ============ await conn.execute(""" CREATE TABLE IF NOT EXISTS teacher_reactions ( id SERIAL PRIMARY KEY, reaction_type VARCHAR(50) NOT NULL, text_pl TEXT NOT NULL, text_ru TEXT NOT NULL, text_uk TEXT NOT NULL, text_en TEXT NOT NULL ) """) # ============ Индекс для teacher_reactions ============ await conn.execute("CREATE INDEX IF NOT EXISTS idx_teacher_reactions_type ON teacher_reactions(reaction_type)") # ============ teacher_chat_history ============ await conn.execute(""" CREATE TABLE IF NOT EXISTS teacher_chat_history ( id BIGSERIAL PRIMARY KEY, user_id BIGINT NOT NULL UNIQUE, messages JSONB NOT NULL DEFAULT '[]', updated_at TIMESTAMP DEFAULT NOW() ) """) # ============ teacher_ai_usage ============ await conn.execute(""" CREATE TABLE IF NOT EXISTS teacher_ai_usage ( id BIGSERIAL PRIMARY KEY, user_id BIGINT NOT NULL, usage_date DATE DEFAULT CURRENT_DATE, command_count INTEGER DEFAULT 0, UNIQUE(user_id, usage_date) ) """) # ============ daily_phrases — фраза дня ============ await conn.execute(""" CREATE TABLE IF NOT EXISTS daily_phrases ( id SERIAL PRIMARY KEY, phrase_date DATE UNIQUE NOT NULL, phrase_pl TEXT NOT NULL, translations JSONB NOT NULL DEFAULT '{}', grammar_breakdowns JSONB NOT NULL DEFAULT '{}', created_at TIMESTAMP DEFAULT NOW() ) """) # ============ Индекс для daily_phrases ============ await conn.execute("CREATE INDEX IF NOT EXISTS idx_daily_phrases_date ON daily_phrases(phrase_date DESC)") # ============ СИСТЕМА ПОДДЕРЖКИ ============ # ============ support_tickets ============ await conn.execute(""" CREATE TABLE IF NOT EXISTS support_tickets ( id BIGSERIAL PRIMARY KEY, user_id BIGINT NOT NULL, status TEXT DEFAULT 'open', created_at TIMESTAMP DEFAULT NOW(), updated_at TIMESTAMP DEFAULT NOW() ) """) # ============ Индекс для support_tickets ============ await conn.execute("CREATE INDEX IF NOT EXISTS idx_support_tickets_user ON support_tickets(user_id)") await conn.execute("CREATE INDEX IF NOT EXISTS idx_support_tickets_status ON support_tickets(status)") # ============ support_messages ============ await conn.execute(""" CREATE TABLE IF NOT EXISTS support_messages ( id BIGSERIAL PRIMARY KEY, ticket_id BIGINT NOT NULL REFERENCES support_tickets(id) ON DELETE CASCADE, sender_id BIGINT NOT NULL, message TEXT NOT NULL, is_read BOOLEAN DEFAULT FALSE, created_at TIMESTAMP DEFAULT NOW() ) """) # ============ Индекс для support_messages ============ await conn.execute("CREATE INDEX IF NOT EXISTS idx_support_messages_ticket ON support_messages(ticket_id)") await conn.execute("CREATE INDEX IF NOT EXISTS idx_support_messages_sender ON support_messages(sender_id)") await conn.execute("CREATE INDEX IF NOT EXISTS idx_support_messages_unread ON support_messages(ticket_id, is_read)") # ============ 🔧 НОВОЕ (18.09.2026): feed_posts — посты для карусели ============ await conn.execute(""" CREATE TABLE IF NOT EXISTS feed_posts ( id BIGSERIAL PRIMARY KEY, section TEXT NOT NULL, source_language TEXT NOT NULL DEFAULT 'ru', title_pl TEXT, title_ru TEXT, title_uk TEXT, title_en TEXT, body_pl TEXT, body_ru TEXT, body_uk TEXT, body_en TEXT, link_url TEXT, telegram_url TEXT, author_id BIGINT NOT NULL, is_pinned BOOLEAN DEFAULT FALSE, is_published BOOLEAN DEFAULT TRUE, created_at TIMESTAMP DEFAULT NOW(), updated_at TIMESTAMP DEFAULT NOW(), edited_at TIMESTAMP, expires_at TIMESTAMP ) """) # ============ Индексы для feed_posts ============ await conn.execute(""" CREATE INDEX IF NOT EXISTS idx_feed_posts_section ON feed_posts(section, created_at DESC) """) await conn.execute(""" CREATE INDEX IF NOT EXISTS idx_feed_posts_pinned ON feed_posts(section, is_pinned DESC, created_at DESC) """) await conn.execute(""" CREATE INDEX IF NOT EXISTS idx_feed_posts_published ON feed_posts(section, is_published) """) # ============ 🔧 НОВОЕ (18.09.2026): feed_attachments — картинки к постам (до 3) ============ await conn.execute(""" CREATE TABLE IF NOT EXISTS feed_attachments ( id BIGSERIAL PRIMARY KEY, post_id BIGINT NOT NULL REFERENCES feed_posts(id) ON DELETE CASCADE, file_path TEXT NOT NULL, file_type TEXT DEFAULT 'image', order_number INT NOT NULL DEFAULT 0, created_at TIMESTAMP DEFAULT NOW() ) """) # ============ Индекс для feed_attachments ============ await conn.execute(""" CREATE INDEX IF NOT EXISTS idx_feed_attachments_post ON feed_attachments(post_id, order_number) """) # ============ user_learning_answers_archive ============ await conn.execute(""" CREATE TABLE IF NOT EXISTS user_learning_answers_archive ( id BIGSERIAL PRIMARY KEY, user_id BIGINT NOT NULL, skill_tag VARCHAR(100) NOT NULL, correct BOOLEAN, wrong_because VARCHAR(100), created_at DATE, archived_at TIMESTAMP DEFAULT NOW() ) """) # ============ Индексы для архива ============ await conn.execute("CREATE INDEX IF NOT EXISTS idx_archive_user_date ON user_learning_answers_archive(user_id, created_at)") await conn.execute("CREATE INDEX IF NOT EXISTS idx_archive_skill_tag ON user_learning_answers_archive(skill_tag)") # ============ Индексы ============ await conn.execute("CREATE INDEX IF NOT EXISTS idx_levels_number ON levels(level_number)") await conn.execute("CREATE INDEX IF NOT EXISTS idx_levels_level ON levels(level)") await conn.execute("CREATE INDEX IF NOT EXISTS idx_levels_level_number ON levels(level, level_number)") await conn.execute("CREATE INDEX IF NOT EXISTS idx_user_progress_user ON user_progress(user_id)") await conn.execute("CREATE INDEX IF NOT EXISTS idx_level_words_level_id ON level_words(level_id)") await conn.execute("CREATE INDEX IF NOT EXISTS idx_level_words_level ON level_words(level)") await conn.execute("CREATE INDEX IF NOT EXISTS idx_users_telegram ON users(telegram_id)") await conn.execute("CREATE INDEX IF NOT EXISTS idx_sentences_number ON sentences(sentence_number)") await conn.execute("CREATE INDEX IF NOT EXISTS idx_sentences_level ON sentences(level)") await conn.execute("CREATE INDEX IF NOT EXISTS idx_sentences_level_number ON sentences(level, sentence_number)") await conn.execute("CREATE INDEX IF NOT EXISTS idx_sentence_progress_user ON sentence_progress(user_id)") await conn.execute("CREATE INDEX IF NOT EXISTS idx_typing_progress_user ON typing_progress(user_id)") await conn.execute("CREATE INDEX IF NOT EXISTS idx_user_progress_score ON user_progress(score DESC)") await conn.execute("CREATE INDEX IF NOT EXISTS idx_sentence_progress_score ON sentence_progress(score DESC)") await conn.execute("CREATE INDEX IF NOT EXISTS idx_user_achievements_user ON user_achievements(user_id)") await conn.execute("CREATE INDEX IF NOT EXISTS idx_daily_logins_user ON daily_logins(user_id)") await conn.execute("CREATE INDEX IF NOT EXISTS idx_daily_logins_date ON daily_logins(login_date DESC)") await conn.execute("CREATE INDEX IF NOT EXISTS idx_learning_topics_level ON learning_topics(level_code)") await conn.execute("CREATE INDEX IF NOT EXISTS idx_learning_lessons_topic ON learning_lessons(topic_id)") await conn.execute("CREATE INDEX IF NOT EXISTS idx_learning_questions_lesson ON learning_questions(lesson_id)") await conn.execute("CREATE INDEX IF NOT EXISTS idx_learning_questions_topic ON learning_questions(topic_id)") await conn.execute("CREATE INDEX IF NOT EXISTS idx_learning_answers_user ON user_learning_answers(user_id, created_at DESC)") await conn.execute("CREATE INDEX IF NOT EXISTS idx_skill_mastery_user ON user_skill_mastery(user_id)") logger.info("Database initialized")