كل ملاحظات البناء

Selekta: محرّر استعلامات واحد لـ PostgreSQL وMySQL وSQLite وSQL Server

Selekta

Selekta هو عميل قواعد البيانات الذي بنيناه. وهو في مرحلة تجريبية عامة على Mac، ويتصل بقواعد PostgreSQL وMySQL وSQLite وSQL Server من واجهة واحدة مألوفة: شجرة تنقّل على الجانب، وألسنة لمحرّر SQL، وشبكة لعرض النتائج. والهدف الذي كتبناه في البداية سهل القول وأصعب في التنفيذ: مشغّلات Rust أصلية بدلًا من JDBC على آلة JVM، كي يُفتح العميل فورًا ويبقى خفيفًا.

يتناول هذا المقال ما يتطلّبه جعل محرّر واحد يعمل جيدًا مع أربعة محرّكات كلها «تتكلم SQL» وتختلف في كل شيء تقريبًا عدا ذلك.

بنية التطبيق

Selekta تطبيق مبني بإطار Tauri. الواجهة مكتوبة بـ Svelte وتعمل داخل عرض الويب الخاص بنظام التشغيل؛ وكل ما يلمس قاعدة البيانات مكتوب بـ Rust. وتستخدم طبقة قواعد البيانات مكتبة sqlx لـ PostgreSQL وMySQL وSQLite، ومكتبة tiberius لـ SQL Server.

تتواصل الواجهة مع Rust عبر غلاف صغير محدَّد الأنواع، ولكل لسان في المحرّر اتصاله المخصّص، فلا يعطّل استعلام طويل في لسان لسانًا آخر. والقاعدة التي نُلزم بها الشيفرة مكتوبة في أعلى وحدة المشغّلات: الواجهة لا تتفرّع أبدًا حسب المحرّك فيما يخص SQL. فالاختلافات بين المحرّكات مكانها Rust، وفي موضع واحد.

خطّطنا لسمة (trait) وأطلقنا enum

كان أول تصوّر للبنية يحمل الإجابة المعتادة في الكتب: سمة Driver فيها connect وlist_schemas وlist_tables وغيرها، وتنفيذ واحد لكل محرّك.

أما ما أُطلق فعلًا فهو enum بسيط فيه متغيّر واحد لمجمّع اتصالات كل محرّك، وعبارات match توجّه كل استدعاء إلى الوحدة الصحيحة. والسبب أن المحرّكات تختلف أكثر مما تحتمله سمة مشتركة. خذ شجرة التنقّل مثلًا. في PostgreSQL وSQL Server تنتقل من قاعدة البيانات إلى المخطط (schema) ثم إلى مجلدات الجداول وطرق العرض. أما في MySQL فقاعدة البيانات هي المخطط، فتنتقل مباشرةً من قاعدة البيانات إلى المجلدات. ولدى SQLite قاعدة بيانات واحدة بالضبط اسمها main. السمة إما أن تُخفي هذه الاختلافات خلف قيم افتراضية متكلّفة، أو أن تسرّبها إلى كل دالة. أما match الصريح فيُبقي مسار كل محرّك مقروءًا وفي ملف واحد.

الإلغاء أربع ميزات مختلفة

في كل عميل لقواعد البيانات زر «إلغاء». وخلف الزر في Selekta أربع آليات منفصلة:

  • PostgreSQL: يطلب Selekta من الخادم إلغاء العملية الجارية عبر pg_cancel_backend.
  • MySQL: أمر KILL QUERY على الاتصال الجاري.
  • SQL Server: رسالة Attention من بروتوكول TDS على الاتصال نفسه. ولا تحتاج إلى أي صلاحية إضافية، على عكس KILL. لكن المشكلة أن الاتصال قد يتأخّر بردّ واحد عن الآخر. لذا بعد الإلغاء، يرسل Selekta استعلامًا صغيرًا يحمل معرّفًا عشوائيًا جديدًا ويتأكّد أن الرد الذي يعود إليه هو رده. وإن لم يكن كذلك، يُستبدل الاتصال.
  • SQLite: يتوقّف عن قراءة الصفوف بين صف وآخر. ولا يمكن مقاطعة عبارة واحدة طويلة في منتصفها؛ وهذا قيد معروف في النسخة التجريبية.

وفي المحرّكات التي تعمل كخوادم توجد أيضًا مهلة سماح: إذا لم تتوقف العبارة بعد ثلاث ثوانٍ من الإلغاء، يُسقَط الاتصال من جهتنا لتعود الألسنة صالحة للاستخدام.

إبقاء الأرقام دقيقة

عميل قواعد بيانات يقرّب أرقامك أسوأ من عديم الفائدة. يشغّل Selekta العبارات عبر بروتوكول النص البسيط، فترسل PostgreSQL وMySQL القيم بصيغتها النصية كما يعرضها الخادم: دقيقة في الأرقام، والتواريخ بصيغة قريبة من ISO تحتفظ بفارق المنطقة الزمنية. وتبقى الأعداد الصحيحة والعشرية نصوصًا حتى تصل إلى الشبكة، فلا تمرّ أبدًا بعدد ذي فاصلة عائمة في طريقها إلى شاشتك.

واحتاج SQL Server إلى عناية إضافية، لأن المشغّل يسلّم نوع money على شكل قيمة ذات فاصلة عائمة لا تستطيع تمثيل أكبر المبالغ بدقة. لذلك قبل تشغيل استعلام SELECT، يطلب Selekta من الخادم وصف أعمدة النتيجة دون تشغيل الاستعلام. فإن كان أحدها من نوع money، يغلّف الاستعلام لتعود تلك الأعمدة أعدادًا عشرية دقيقة، ويطلب من الخادم التحقق من النسخة المغلّفة، ثم يشغّلها بعد ذلك فقط. ويكلّف هذا رحلة ذهاب وإياب إضافية لكل SELECT (أو اثنتين عند وجود أعمدة money)، ويُتخطّى داخل المعاملة إذا كان خطأ في الوصف قد يُفسد عملك. وتُحلَّل البيانات المكانية من PostGIS وMySQL بشيفرتنا الخاصة، فتُعرض القيم المكانية على خريطة بدلًا من أرقام سداسية عشرية.

بثّ مليون صف

كان من أهدافنا أن تُمرَّر نتيجة من مليون صف بمعدل 60 إطارًا في الثانية دون تأخير. تتدفّق الصفوف من Rust إلى الواجهة عبر قناة Tauri على دفعات، ولا ترسم الشبكة إلا الصفوف الظاهرة لك، مع هامش صغير فوقها وتحتها.

أظهر أول قياس لأداء «جلب الكل» على مليون صف أن خيط الواجهة يقضي 60–100 ملّي ثانية في كل إطار. وكان هناك خطآن:

  1. كانت الواجهة تنسخ النتيجة كاملة في كل إطار. فكل إطار رسوم كان يضيف الصفوف الجديدة بدمج المصفوفات. وعند مليون صف يعني ذلك نسخًا كاملًا نحو 75 مرة في الثانية — أي عملًا تربيعيًا في المجمل. وفوق 50,000 صف صارت الواجهة تنشر الصفوف الجديدة أربع مرات في الثانية على الأكثر.
  2. كانت كل دفعة أكبر قليلًا من اللازم. كانت الصفوف تُرسل 100 صف في كل مرة. وكان حدث المئة صف يبلغ نحو 11 كيلوبايت، أي فوق حد Tauri البالغ 8 كيلوبايت لتسليم الرسالة مباشرةً، فكلّفت كل دفعة رحلة ذهاب وإياب إضافية. وفي «جلب الكل» يرسل Selekta الآن 1,000 صف في كل حدث: 1,000 رحلة لمليون صف بدلًا من 10,000. أما النتائج العادية المقسّمة إلى صفحات فما زالت تُرسل 100 صف في كل مرة.

ما قسناه، بما في ذلك الإخفاقات

نحتفظ بنص لقياس الأداء داخل المستودع ونسجّل نتائجه، نجاحًا كانت أو إخفاقًا. على جهاز Apple M4 وبإصدار مبكر (0.1.1):

  • جلب 1,000,000 صف كاملة: 0.8–1.2 ثانية، وفي التمرير السلس عبرها لم يسقط أي إطار من أصل 600.
  • الذاكرة في وضع الخمول دون اتصال: 90 ميغابايت، مقابل هدف قدره 150 ميغابايت.
  • المُثبِّت: 11.3 ميبيبايت لمعالجات Apple، مقابل هدف قدره 30 ميغابايت.
  • التشغيل الدافئ: 0.34–0.64 ثانية، مقابل هدف قدره 1.5 ثانية.

أما الإخفاقات: فأول تشغيل بعد التثبيت أو التحديث استغرق 1.55–2.38 ثانية، معظمها في فحص macOS لملف تنفيذي مربوط حديثًا — كلفة تُدفع مرة واحدة، لكنها تتجاوز هدفنا. واختبار التمرير «الكاسح» القاسي عمدًا ما زال يُظهر توقفًا ملحوظًا واحدًا. ومع وجود اتصال مفتوح، والنافذة في المقدمة، والمؤشر داخل حقل نصي، قد تصل الذاكرة إلى نحو 190 ميغابايت. ونفضّل تدوين هذه الأمور على التظاهر بأنها غير موجودة.

الحرص على بيانات الآخرين

يجلس عميل قواعد البيانات بجوار بيانات الإنتاج، لذا فبعض القرارات تتعلق بالأمان لا بالسرعة:

  • كلمات المرور تُحفظ في سلسلة مفاتيح نظام التشغيل، لا في قاعدة إعدادات Selekta نفسه.
  • الاتصالات للقراءة فقط مفروضة مرتين: بإعداد القراءة فقط الخاص بالمحرّك على جانب الخادم، وبفحص يرفض عبارات الكتابة قبل إرسالها.
  • العبارات المدمِّرة تطلب التأكيد أولًا، وعند تعديل البيانات أو البنية في الشبكة، يعرض لك Selekta استعلام SQL الذي سيشغّله قبل تشغيله. ولا يُسمح بتعديل الصفوف إلا في الجداول التي تملك مفتاحًا أساسيًا أو فريدًا، كي لا يصيب التعديل صفًا خاطئًا.
  • النسخ الاحتياطي لا يضع كلمات المرور في سطر الأوامر أبدًا. عندما يشغّل Selekta أداة pg_dump أو mysqldump، تنتقل كلمة المرور إلى العملية الفرعية عبر متغيرات البيئة، لا عبر الوسائط، ولا تُكتب في أي سجل.

أين يقف الآن

Selekta ما زال في مرحلة تجريبية، وبعض أجزائه غير مكتملة: فاستيراد CSV لا يستطيع إنشاء أعمدة جديدة بعد، وبعض المحرّرات الخاصة بمحرّكات معيّنة لم تنتهِ. إذا كنت تعمل مع قواعد البيانات على Mac، يمكنك تنزيله من selekta.kreasimaju.id. وأخبرنا بما يتعطّل.