Skip to content
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
117 changes: 117 additions & 0 deletions docs/sql_migration_plan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
# خطة الهجرة واستبدال استخدام mdapi_sql/services بنظام SQLAlchemy الجديد

تهدف هذه الخطة إلى تفصيل عملية استبدال المكونات القديمة المبنية على الاستعلامات المباشرة (Raw SQL) المتمثلة في `src/db/mdapi_sql/services/` والاعتماد بشكل كامل على نظام الخدمات الجديد المبني على **SQLAlchemy ORM** والموجود تحت `src/db/tools/services/`.

---

## 1. الأهداف والفوائد
* **أمان البيانات**: التخلص الكامل من احتمالية ثغرات حقن استعلامات SQL (SQL Injection).
* **إدارة الاتصال**: الاستفادة من ميزة تجميع الاتصالات (Connection Pooling) التي يوفرها SQLAlchemy بدلاً من فتح وإغلاق الاتصال مع كل استعلام.
* **جودة الشيفرة وقابليتها للصيانة**: استخدام الكائنات البرمجية المقابلة للجداول (ORM Models) مما يسهل عمليات التطوير وفحص الكود (Testing).
* **التكامل مع الاختبارات**: سهولة إجراء اختبارات الوحدات (Unit Tests) باستخدام قواعد بيانات وهمية في الذاكرة (SQLite in-memory) لخدمات SQLAlchemy.

---

## 2. جدول مطابقة الدوال والملفات (Mapping Table)

### أ. ملف `sql_qids.py` -> `qid_service.py`
المسار القديم: `src/db/mdapi_sql/services/sql_qids.py`
المسار الجديد: `src/db/tools/services/wikidata/qid_service.py`

| الدالة القديمة | البديل باستخدام SQLAlchemy / الدالة الجديدة | ملاحظات / خطة التعديل |
| :--- | :--- | :--- |
| `get_all_qids()` | `get_title_to_qid()` | ترجع قاموساً يربط العناوين بمعرفات QID مباشرة باستخدام الاستعلام عبر الـ ORM. |
| `add_qid(title, qid)` | `insert(title, qid)` أو `add_qid(title, qid)` | تقوم بالإدخال والتحقق من عدم الوجود المسبق. سنقوم بدعم معايير الفحص الإضافية في الخدمة الجديدة. |
| `set_qid_where_qid(new_qid, old_qid)` | دالة جديدة: `set_qid_where_qid(new_qid, old_qid)` | تحديث جميع السجلات التي تمتلك معرف QID قديم إلى المعرف الجديد باستخدام `session.query(QidRecord).filter(...)`. |
| `set_qid_where_title(title, qid)` | `add_qid(title, qid)` أو دالة مخصصة | تحديث المعرف بناءً على عنوان الصفحة. |
| `delete_title_from_db(title)` | دالة جديدة: `delete_by_title(title)` | حذف السجل المطابق للعنوان باستخدام `session.query(QidRecord).filter_by(title=title).delete()`. |
| `set_title_where_qid(new_title, qid)` | دالة جديدة: `set_title_where_qid(new_title, qid)` | تحديث عنوان الصفحة المقترن بالـ QID. |
| `qids_set_title_where_title_qid(...)` | دالة جديدة: `set_title_where_title_qid(...)` | تحديث مخصص للعنوان لـ QID محدد وعنوان قديم معين. |
| `add_titles_to_qids(tab0, add_empty_qid)`| دالة جديدة: `add_titles_to_qids(...)` | نقل نفس المنطق البرمجي للتحقق من العناوين وإضافتها دفعة واحدة باستخدام الـ ORM وإدارتها ضمن جلسة عمل (Session) واحدة لضمان الأداء العالي. |

---

### ب. ملف `sql_qids_others.py` -> `qid_others_service.py`
المسار القديم: `src/db/mdapi_sql/services/sql_qids_others.py`
المسار الجديد: `src/db/tools/services/wikidata/qid_others_service.py`

| الدالة القديمة | البديل باستخدام SQLAlchemy / الدالة الجديدة | ملاحظات / خطة التعديل |
| :--- | :--- | :--- |
| `get_others_qids()` | `get_title_to_qid()` | مستقاة بالكامل من جدول `qids_others` عبر الـ ORM. |
| `add_qid(title, qid)` | `insert(title, qid)` أو `add_qid_other(title, qid)` | تقوم بالإدخال والتحقق من عدم الوجود المسبق في كلا الجدولين. |

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The note states that the alternative functions insert or add_qid_other insert and check for pre-existence in both tables (qids and qids_others).

However, looking at the current implementation of qid_others_service.py, both insert and add_qid_other only query the qids_others table and do not perform any checks on the qids table (unlike the legacy add_qid in sql_qids_others.py which used a NOT EXISTS clause on both tables).

To prevent duplicate or inconsistent records across tables, please update the note to clarify that the current service functions do not support this double-check yet, and indicate that the service needs to be updated before migration.

Suggested change
| `add_qid(title, qid)` | `insert(title, qid)` أو `add_qid_other(title, qid)` | تقوم بالإدخال والتحقق من عدم الوجود المسبق في كلا الجدولين. |
| add_qid(title, qid) | دالة جديدة أو تحديث الخدمة الحالية | تقوم بالإدخال والتحقق من عدم الوجود المسبق في كلا الجدولين (تحتاج الخدمة الحالية للتحديث لدعم الفحص المزدوج). |

| `set_qid_where_qid(new_qid, old_qid)` | دالة جديدة: `set_qid_where_qid(...)` | تحديث السجلات في جدول `qids_others` عبر الاستعلام المباشر بالـ ORM. |
| `set_qid_where_title(title, qid)` | دالة جديدة أو استخدام `add_qid_other` | تحديث المعرف بناءً على عنوان الصفحة. |
| `delete_title_from_db(title)` | دالة جديدة: `delete_by_title(title)` | حذف السجل المطابق للعنوان من جدول `qids_others`. |
| `set_title_where_qid(new_title, qid)` | دالة جديدة: `set_title_where_qid(...)` | تحديث عنوان الصفحة المقترن بالـ QID في جدول `qids_others`. |
| `qids_set_title_where_title_qid(...)` | دالة جديدة | تحديث مخصص للعنوان لـ QID وعنوان قديم في جدول `qids_others`. |
| `add_titles_to_qids(...)` | دالة جديدة | نقل المنطق الذكي للإضافات الدفعية لجدول `qids_others`. |

---

### ج. ملف `sql_for_mdwiki.py` -> خدمات نطاقات SQLAlchemy المتعددة
المسار القديم: `src/db/mdapi_sql/services/sql_for_mdwiki.py`
المسار الجديد: موزعة على خدمات النطاقات في `src/db/tools/services/`

| الدالة القديمة في `sql_for_mdwiki.py` | الخدمة البديلة في نظام SQLAlchemy الجديد | ملاحظات ومسار الاستبدال |
| :--- | :--- | :--- |
| `mdwiki_sql` / `mdwiki_sql_dict` | `get_session()` مع تنفيذ استعلام نصي (Raw Text execution) أو استخدام الـ ORM مباشرة | لا يجب استخدام الاستعلامات المباشرة إلا للضرورة القصوى. يُفضل تحويل كل استعلام إلى ما يقابله من كائنات ORM. |
| `get_all_pages()` | `pages/page_service.py` -> `list_pages()` | يمكن تصفية العناوين فقط كـ `[p.title for p in list_pages()]` أو إضافة دالة خفيفة مخصصة للعناوين فقط لتقليل استهلاك الذاكرة. |
| `get_all_from_table(table_name)` | استعلام ORM عام أو دالة مخصصة لكل جدول | جلب البيانات باستخدام كائن الـ Model المقابل لاسم الجدول مثل `session.query(Model).distinct().all()`. |
| `get_all_pages_all_keys(...)` | `pages/page_service.py` / `pages/user_page_service.py` | استخدام `list_pages()` أو `list_user_pages()` وتصفيتها باللغة المطلوبة عبر فلتر SQLAlchemy. |
| `get_db_categories()` | `content/category_service.py` -> `list_categories()` | قراءة الجداول ككائنات `CategoryRecord` وتحويلها لقاموس يربط التصنيف بالعمق (depth). |
| `get_db_category_members()` | خدمة جديدة في `content/category_service.py` أو الـ Model المقابل | الاستعلام عن علاقة التصنيفات بالصفحات عبر الـ ORM المخصص لـ `category_members` لتحسين جودة الترابط. |
| `get_db_users()` | `pages/user_page_service.py` أو دالة مستخدم مخصصة | جلب قائمة أسماء المستخدمين المميزين المسجلين في جدول `users`. |

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The legacy function get_db_users() queries the users table (which contains approved/registered usernames), whereas the proposed service pages/user_page_service.py manages the pages_users table (represented by UserPageRecord, which contains page translation records by users).

These two tables have completely different structures and purposes. It is highly recommended to point to a dedicated user service (e.g., users/user_service.py or similar) instead of directing developers to user_page_service.py.

Suggested change
| `get_db_users()` | `pages/user_page_service.py` أو دالة مستخدم مخصصة | جلب قائمة أسماء المستخدمين المميزين المسجلين في جدول `users`. |
| get_db_users() | خدمة مستخدمين مخصصة (مثال: users/user_service.py) | جلب قائمة أسماء المستخدمين المميزين المسجلين في جدول users. |

| `set_target_where_id(new_target, iid)` | `pages/page_service.py` -> `update_page(page_id=iid, target=new_target)` | تعديل السجل وتحديث حقل `target` وحقل `pupdate` تلقائياً عبر الـ ORM. |

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The proposed function update_page in page_service.py has the following signature:

def update_page(
    page_id: int,
    title: str,
    target: str,
    **kwargs: Any,
) -> PageRecord:

Both title and target are required positional arguments.

Calling update_page(page_id=iid, target=new_target) as suggested in the table will raise a TypeError because the required title argument is missing.

Please clarify this in the notes, or suggest using set_page_target after retrieving the record, or modify update_page to make those parameters optional.

Suggested change
| `set_target_where_id(new_target, iid)` | `pages/page_service.py` -> `update_page(page_id=iid, target=new_target)` | تعديل السجل وتحديث حقل `target` وحقل `pupdate` تلقائياً عبر الـ ORM. |
| set_target_where_id(new_target, iid) | pages/page_service.py -> set_page_target(...) بعد جلب السجل | تعديل السجل وتحديث حقل target وحقل pupdate تلقائياً عبر الـ ORM (يتطلب جلب كائن PageRecord أولاً). |

| `set_deleted_where_id(iid)` | `pages/page_service.py` -> `update_page(page_id=iid, deleted=1)` | وسم الصفحة كـ "محذوفة" من خلال تحديث حقل `deleted` المقابل للـ ID المعطى. |

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Similar to the previous comment, update_page requires both title and target as positional arguments.

Calling update_page(page_id=iid, deleted=1) will fail with a TypeError.

It is recommended to suggest creating a dedicated soft-delete function (e.g., delete_page_by_id(page_id)) or updating update_page to support partial updates.

Suggested change
| `set_deleted_where_id(iid)` | `pages/page_service.py` -> `update_page(page_id=iid, deleted=1)` | وسم الصفحة كـ "محذوفة" من خلال تحديث حقل `deleted` المقابل للـ ID المعطى. |
| set_deleted_where_id(iid) | pages/page_service.py -> دالة جديدة أو تحديث جزئي | وسم الصفحة كـ "محذوفة" من خلال تحديث حقل deleted المقابل للـ ID المعطى (يتطلب دالة تدعم التحديث الجزئي دون إلزامية العنوان والهدف). |

| `insert_to_pages_users_to_main(...)` | `reports/pages_users_to_main_service.py` -> `add_pages_users_to_main(...)` | إدراج السجل الجديد والتحقق من نجاح العملية باستخدام الـ ORM. |
| `add_new_to_pages(tab)` | `pages/page_service.py` -> `add_page(...)` أو `add_translate_row_to_db(...)` | استخدام دوال الإضافة الجاهزة في خدمة الصفحات والتي تتكفل بالتعامل مع التواريخ والتحققات التلقائية. |

---

## 3. خطوات التنفيذ التدريجي (Implementation Phases)

### المرحلة الأولى: تحديث وتحضير خدمات SQLAlchemy (Enhancement Phase)
1. **تحديث `qid_service.py` و `qid_others_service.py`**:
- إضافة الدوال الغائبة مثل تحديث الـ QID أو العنوان بناءً على الـ QID، وحذف العناوين.
- نقل منطق `add_titles_to_qids` الذكي الذي يمنع التكرارات ويسرع الإدخال ليعمل بالكامل عبر الـ ORM.
2. **تحديث خدمات الصفحات والتصنيفات**:
- إضافة دالة `get_db_categories()` في `category_service.py` لترجع قاموس التصنيفات وعمقها مباشرة تسهيلاً لعملية الاستبدال السريع.
- كتابة خدمات لإدارة `category_members` وجلب أعضاء التصنيفات ككائنات ORM.

### المرحلة الثانية: استبدال الاستدعاءات في الـ Bots والـ Scripts (Refactoring Callers)
البحث عن كافة الملفات التي تستورد من `db.mdapi_sql.services` وتغييرها لتستورد من الخدمات المقابلة في `db.tools.services`.

أمثلة على الاستبدال:

* **تعديل استيراد معرفات الويكي بيانات (Wikidata QIDs)**:
```python
# القديم
from db.mdapi_sql.services import sql_qids, sql_qids_others

# الجديد
from db.tools.services.wikidata import qid_service, qid_others_service
```

* **تعديل استيراد خدمات صفحات mdwiki**:
```python
# القديم
from db.mdapi_sql.services import sql_for_mdwiki

# الجديد
from db.tools.services.pages import page_service
from db.tools.services.content import category_service
```

### المرحلة الثالثة: إزالة المكونات القديمة بالكامل (Cleanup Phase)
1. بعد تحويل كافة الملفات والتأكد من عدم وجود أي استدعاء يشير إلى `src/db/mdapi_sql/` أو ملفاتها الداخلية.
2. إزالة المجلد `src/db/mdapi_sql/` من شجرة المشروع نهائياً لتفادي وجود أكواد مكررة أو غير مستخدمة.

---

## 4. استراتيجية فحص الكود وضمان الجودة (Testing & Quality Assurance)
* **كتابة اختبارات الوحدات (Unit Tests)**:
- إضافة اختبارات وحدات في مجلد `tests/` للتحقق من أن الخدمات الجديدة في SQLAlchemy تعطي نفس النتائج تماماً للبيانات المدخلة مقارنة بالدوال القديمة.
* **الاختبار اليدوي لبعض المهام (Smoke Testing)**:
- تشغيل عينة من الـ bots والـ scripts المعدلة والتأكد من عدم حدوث أي خطأ متعلق بالاتصال بقاعدة البيانات أو معالجة الحقول.
* **الفحص التلقائي (Automated Verification)**:
- تشغيل أداة `pytest` للتحقق من سلامة كافة الاختبارات وتوافقها مع التغييرات المطبقة.