Files
filwordspl-backend/database.py
T
2026-09-20 11:45:58 +00:00

870 lines
37 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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")