# ============================================ # database/migrations.py — إنشاء الجداول والترقية # ============================================ # لماذا migrations وليس init فقط؟ # لأن المشروع سيتوسع وستُضاف أعمدة جديدة لاحقاً. # النمط هنا: كل جدول يُنشأ بـ IF NOT EXISTS وكل عمود # جديد يُضاف بـ ADD COLUMN IF NOT EXISTS — آمن على # قواعد البيانات الموجودة (لا تمسح البيانات). # ============================================ import logging from database.connection import get_connection logger = logging.getLogger(__name__) def init_database() -> None: """ إنشاء أو ترقية كل الجداول والفهارس. آمن للاستدعاء عند كل إقلاع — لن يمسح بيانات موجودة. """ conn = get_connection() c = conn.cursor() try: # ──────────────────────────────────────── # جدول الرسائل المفهرسة (الأساسي) # ──────────────────────────────────────── c.execute(""" CREATE TABLE IF NOT EXISTS messages ( id INTEGER PRIMARY KEY AUTOINCREMENT, message_id INTEGER NOT NULL, channel_id INTEGER NOT NULL, channel_name TEXT DEFAULT '', channel_username TEXT DEFAULT '', message_text TEXT DEFAULT '', text_normalized TEXT DEFAULT '', message_date TEXT NOT NULL, hashtags TEXT DEFAULT '', platform TEXT DEFAULT '', has_media INTEGER DEFAULT 0, media_type TEXT DEFAULT '', indexed_at TEXT DEFAULT (datetime('now','localtime')), UNIQUE(message_id, channel_id) ) """) # ──────────────────────────────────────── # فهرس البحث الكامل FTS5 # ──────────────────────────────────────── c.execute(""" CREATE VIRTUAL TABLE IF NOT EXISTS messages_fts USING fts5( message_text, text_normalized, channel_name, hashtags, platform, content='messages', content_rowid='id', tokenize='unicode61' ) """) # ──────────────────────────────────────── # Triggers للمزامنة التلقائية مع FTS # ──────────────────────────────────────── c.executescript(""" CREATE TRIGGER IF NOT EXISTS fts_insert AFTER INSERT ON messages BEGIN INSERT INTO messages_fts( rowid, message_text, text_normalized, channel_name, hashtags, platform ) VALUES ( new.id, new.message_text, new.text_normalized, new.channel_name, new.hashtags, new.platform ); END; CREATE TRIGGER IF NOT EXISTS fts_delete AFTER DELETE ON messages BEGIN INSERT INTO messages_fts( messages_fts, rowid, message_text, text_normalized, channel_name, hashtags, platform ) VALUES ( 'delete', old.id, old.message_text, old.text_normalized, old.channel_name, old.hashtags, old.platform ); END; CREATE TRIGGER IF NOT EXISTS fts_update AFTER UPDATE ON messages BEGIN INSERT INTO messages_fts( messages_fts, rowid, message_text, text_normalized, channel_name, hashtags, platform ) VALUES ( 'delete', old.id, old.message_text, old.text_normalized, old.channel_name, old.hashtags, old.platform ); INSERT INTO messages_fts( rowid, message_text, text_normalized, channel_name, hashtags, platform ) VALUES ( new.id, new.message_text, new.text_normalized, new.channel_name, new.hashtags, new.platform ); END; """) # ──────────────────────────────────────── # جدول الإعدادات # ──────────────────────────────────────── c.execute(""" CREATE TABLE IF NOT EXISTS settings ( key TEXT PRIMARY KEY, value TEXT NOT NULL ) """) # ──────────────────────────────────────── # جدول المستخدمين (جديد) # ──────────────────────────────────────── c.execute(""" CREATE TABLE IF NOT EXISTS users ( user_id INTEGER PRIMARY KEY, username TEXT DEFAULT '', first_name TEXT DEFAULT '', joined_at TEXT DEFAULT (datetime('now','localtime')), is_banned INTEGER DEFAULT 0 ) """) # ──────────────────────────────────────── # جدول المشتركين في التنبيهات (جديد) # ──────────────────────────────────────── c.execute(""" CREATE TABLE IF NOT EXISTS notification_subscribers ( user_id INTEGER PRIMARY KEY, platform TEXT DEFAULT 'all', subscribed_at TEXT DEFAULT (datetime('now','localtime')), FOREIGN KEY (user_id) REFERENCES users(user_id) ON DELETE CASCADE ) """) # ──────────────────────────────────────── # جدول سجل الإذاعات (جديد) # ──────────────────────────────────────── c.execute(""" CREATE TABLE IF NOT EXISTS broadcasts ( id INTEGER PRIMARY KEY AUTOINCREMENT, admin_id INTEGER NOT NULL, message_text TEXT NOT NULL, broadcast_type TEXT DEFAULT 'manual', game_title TEXT DEFAULT '', game_link TEXT DEFAULT '', sent_count INTEGER DEFAULT 0, failed_count INTEGER DEFAULT 0, blocked_count INTEGER DEFAULT 0, sent_at TEXT DEFAULT (datetime('now','localtime')) ) """) # ──────────────────────────────────────── # حفظ الإعدادات الافتراضية # ──────────────────────────────────────── from config import DEFAULT_CHANNEL_TEXT c.execute( "INSERT OR IGNORE INTO settings (key, value) VALUES (?, ?)", ("channel_text", DEFAULT_CHANNEL_TEXT) ) conn.commit() logger.info("✅ قاعدة البيانات جاهزة (كل الجداول موجودة أو تم إنشاؤها)") except Exception as e: conn.rollback() logger.critical(f"❌ فشل تهيئة قاعدة البيانات: {e}", exc_info=True) raise finally: conn.close() def rebuild_fts_index() -> bool: """إعادة بناء فهرس البحث الكامل — مفيد بعد عمليات حذف جماعية""" conn = get_connection() try: conn.execute("INSERT INTO messages_fts(messages_fts) VALUES('rebuild')") conn.commit() logger.info("✅ تم إعادة بناء فهرس FTS") return True except Exception as e: logger.error(f"❌ فشل إعادة بناء فهرس FTS: {e}", exc_info=True) return False finally: conn.close()