> ## Documentation Index
> Fetch the complete documentation index at: https://private-7c7dfe99-parallel-read-in-order-multi-part.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

> دعم SQLAlchemy وAlembic في ClickHouse

# دعم SQLAlchemy

يوفّر ClickHouse Connect لهجة SQLAlchemy `clickhousedb` المبنية على المشغّل الأساسي. وتدعم اللهجة المتزامنة SQLAlchemy 1.4.40 والإصدارات الأحدث، بما في ذلك SQLAlchemy 2.x، مع التركيز على استعلامات Core، وClickHouse DDL، واستكشاف البنية، وعمليات insert البسيطة في ORM. أما اللهجة غير المتزامنة فتتطلب SQLAlchemy 2.0.44 أو أحدث.

ثبّت تبعيات SQLAlchemy باستخدام الـ extra الخاصة بالحزمة:

```bash theme={null}
pip install "clickhouse-connect[sqlalchemy]"
```

<h2 id="sqlalchemy-connect">
  الاتصال عبر SQLAlchemy
</h2>

أنشئ محركًا باستخدام صيغة URL ‏`clickhousedb://` أو `clickhousedb+connect://`:

```python theme={null}
from sqlalchemy import create_engine, text

engine = create_engine(
    "clickhousedb://user:password@host:8123/mydb?compression=zstd"
)

with engine.connect() as conn:
    version = conn.execute(text("SELECT version()")).scalar_one()
    print(version)
```

<h3 id="sqlalchemy-session-ids">
  معرّفات جلسات ClickHouse
</h3>

افتراضيًا، يُنشئ كل اتصال ضمن مجمّع الاتصالات معرّف جلسة ClickHouse خاصًا به، سواء في اللهجة المتزامنة أو غير المتزامنة. وعندما تصل طلبات هذا الاتصال إلى عملية خادم ClickHouse نفسها، تظل الإعدادات المُغيَّرة باستخدام `SET` والجداول المؤقتة محفوظة لهذا الاتصال. وتجدر الإشارة إلى أن حالة الجلسة المسمّاة وعمليات فحص التداخل ضمن الجلسة نفسها تقتصر على العملية الواحدة. ففي عملية خادم واحدة، يُرفض فورًا أي طلب متداخل للمستخدم نفسه ومعرّف الجلسة نفسه برمز الخادم 373 بدلًا من وضعه في قائمة انتظار. وإذا هيّأت قيمة ثابتة لـ `session_id`، فاستخدم `pool_size=1, max_overflow=0` أو اجعل الوصول تسلسليًا قبل أن تصل الطلبات إلى ClickHouse. أما في ClickHouse Cloud أو غيرها من البيئات التي تعتمد على موازنة الحمل، فقد تصل الطلبات التي تحمل معرّف الجلسة نفسه إلى خوادم مختلفة، لذا لا تعتمد على قيمة ثابتة لـ `session_id` بوصفها حالة موزّعة أو قفل استبعاد متبادل (mutex) موزّعًا.

<h3 id="sqlalchemy-async-connections">
  الاتصالات غير المتزامنة
</h3>

تتطلب اللهجة غير المتزامنة الإصدار 2.0.44 من SQLAlchemy أو أحدث، وتعتمد على `AsyncClient` الأصلي في ClickHouse Connect. ثبّت الاعتماديات اللازمة لها، ثم أنشئ محركًا غير متزامن باستخدام عنوان URL ‏`clickhousedb+async://`:

```bash theme={null}
pip install "clickhouse-connect[sqlalchemy-async]"
```

```python theme={null}
import asyncio

from sqlalchemy import text
from sqlalchemy.ext.asyncio import create_async_engine


async def main():
    engine = create_async_engine(
        "clickhousedb+async://user:password@host:8123/mydb"
    )
    try:
        async with engine.connect() as conn:
            version = (await conn.execute(text("SELECT version()"))).scalar_one()
            print(version)
    finally:
        await engine.dispose()


asyncio.run(main())
```

تُخزَّن النتائج مؤقتًا في الذاكرة. المؤشرات من جانب الخادم معطّلة، ولذلك تُثير `AsyncConnection.stream()` الاستثناء `InvalidRequestError`. يقبل SQLAlchemy استدعاء `AsyncSession.stream()`، لكنّ اللهجة تخزّن النتيجة كاملةً مؤقتًا قبل إعادتها. للنتائج الكبيرة، استخدم طرق الدفق الأصلية في `AsyncClient`. ويمكن الوصول إلى العميل الأصلي الخام عبر `driver_connection` ما دام اتصال SQLAlchemy المرتبط به محجوزًا:

```python theme={null}
async def stream_events(engine):
    async with engine.connect() as conn:
        raw_connection = await conn.get_raw_connection()
        client = raw_connection.driver_connection
        async with await client.query_rows_stream("SELECT * FROM events") as rows:
            async for row in rows:
                print(row)
```

لا تستخدم اتصال SQLAlchemy بالتزامن مع العميل الخام الخاص به. أنهِ تدفقات العميل الخام قبل الخروج من كتلة اتصال SQLAlchemy، ولا تحتفظ بالعميل الخام بعد عودة الاتصال إلى مجمّع الاتصالات. تتولى SQLAlchemy إدارة دورة حياة العميل المستعار، لذا لا تستدعِ أبدًا `client.close()` أو أيًّا من توابع دورة الحياة الخاصة به. ويتولى مجمّع الاتصالات في SQLAlchemy التحكم في تزامن الاتصالات. يمتلك كل اتصال ضمن المجمّع عميلًا أصليًا غير متزامن واحدًا، ويضبط افتراضيًا حدود موصل aiohttp على اتصال واحد إجمالًا واتصال واحد لكل مضيف. لتجاوز إعدادات النقل هذه، اضبط `connector_limit` أو `connector_limit_per_host` أو `keepalive_timeout` في URL أو في `connect_args`. عند استخدام `pool_pre_ping=True`، تتحقق SQLAlchemy من الاتصالات المُعاد استخدامها عبر `SELECT 1` عند سحب اتصال من المجمّع.

ترسل عمليات الإدراج عبر executemany في SQLAlchemy غير المتزامنة حاليًا طلب HTTP واحدًا لكل مجموعة معلمات، بدلًا من استخدام بروتوكول الإدراج المجمّع الأصلي الخاص بالمشغّل. لذا لا تستخدم هذا المسار إلا للدفعات الصغيرة. أما للبيانات الضخمة، فاستخدم نمط الوصول `driver_connection` المملوك للمجمّع والموضح أعلاه، وانتظر اكتمال `client.insert()` قبل إعادة اتصال SQLAlchemy إلى المجمّع. ولأن executemany غير المتزامن يعتمد على ربط معلمات الاستعلام، فإن قيم `datetime` المجرّدة من المنطقة الزمنية تخضع للإعداد `naive_datetime_binding`، لا للإعداد `naive_datetime_insert` المستخدم في executemany الأصلي المتزامن. تحافظ عمليات ربط `DateTime64` محددة النوع في SQLAlchemy على أجزاء الثانية، سواء مع المعلمات من جهة العميل أو من جهة الخادم. أما المعلمات غير محددة النوع `%s` أو `%(name)s` الممرَّرة إلى `exec_driver_sql()` فتحتفظ بالتنسيق الافتراضي بالثواني الكاملة لقيم `datetime` المجرّدة من المنطقة الزمنية. استخدم قيمًا تتضمن المنطقة الزمنية لضمان سلوك واضح لا لبس فيه. واستخدم `client.insert()` للحصول على دلالات الإدراج المجمّع الأصلي.

أنشئ المحرك غير المتزامن وتخلّص منه داخل حلقة الأحداث نفسها التي يُستخدم فيها. أعِد كل اتصال مسحوب، ثم انتظر `engine.dispose()` أثناء الإيقاف وقبل استخدام المحرك من حلقة أحداث أخرى. وإذا كانت الحلقة المالكة للمحرك قد أُغلقت بالفعل، فانتظر `engine.dispose()` في الحلقة الحالية قبل إعادة استخدامه. قد يُبلغ aiohttp عن وسيلة نقل غير مغلقة إذا لم يبدأ التنظيف إلا بعد إغلاق الحلقة المالكة، لذا تخلّص من المحرك قبل نقله كلما أمكن. لا يُغني `pool_pre_ping=True` عن التخلّص من المحرك عند نقل محرك غير متزامن يستخدم مجمّع اتصالات بين حلقات الأحداث. ولمشاركة محرك واحد عبر حلقات أحداث متعددة دون الاحتفاظ باتصالات مرتبطة بحلقة معينة، اضبط `poolclass=NullPool`. وإذا جرى التخلّص بينما لا يزال أحد الاتصالات مسحوبًا، تغلق اللهجة ذلك الاتصال عند إعادته أو عند جمعه ضمن جمع المهملات. لا تستدعِ `engine.sync_engine.dispose()` من شيفرة متزامنة؛ إذ لا تستطيع SQLAlchemy في هذه الحالة انتظار تنظيف الاتصالات غير المتزامنة، وقد تكتفي بتسجيل الخطأ بدلًا من إغلاق وسائل النقل في المجمّع.

يمكن أن تتضمن معلمات استعلام URL إعدادات ClickHouse، أو خيارات عميل ClickHouse Connect مثل `compression` و`query_limit` ومهلات الانتظار، أو خيارات HTTP/TLS مثل `ca_cert`. أضف البادئة `ch_` إلى إعداد ClickHouse لفرض معاملته كإعداد خادم عند الحاجة، مثل `ch_http_max_field_name_size=99999`.

راجع [وسائط الاتصال والإعدادات](/ar/integrations/language-clients/python/driver-api#connection-arguments) للاطلاع على خيارات العميل المتاحة.

شغّل أدوات SQLAlchemy المساعدة المتزامنة، مثل DDL والفحص، عبر `AsyncConnection.run_sync()`:

```python theme={null}
from sqlalchemy import inspect


async def prepare_schema(engine, metadata):
    async with engine.begin() as conn:
        await conn.run_sync(metadata.create_all)
        return await conn.run_sync(
            lambda sync_conn: inspect(sync_conn).get_table_names()
        )
```

<h3 id="sqlalchemy-per-query-settings">
  إعدادات خاصة بكل استعلام
</h3>

مرِّر إعدادات ClickHouse عبر خيارات التنفيذ في SQLAlchemy. يمكن تعيين الإعدادات على المحرك أو الاتصال أو التعليمة. وتكون قيمة التعليمة لها الأسبقية على قيمة الاتصال أو المحرك عند استخدام المفتاح نفسه.

```python theme={null}
from sqlalchemy import text

stmt = text("SELECT getSetting('max_threads')").execution_options(
    settings={"max_threads": 2}
)

with engine.connect() as conn:
    value = conn.execute(stmt).scalar_one()
```

<h3 id="sqlalchemy-per-query-read-formats">
  تنسيقات القراءة لكل استعلام
</h3>

عيّن تنسيقات قراءة ClickHouse على مستوى المحرك أو الاتصال أو التعليمة عبر خيارات تنفيذ SQLAlchemy باستخدام `query_formats`. تُطبَّق تنسيقات التعليمة أولًا، لذا تتجاوز المفاتيح وأحرف البدل المطابقة على مستوى الاتصال أو المحرك.

```python theme={null}
from sqlalchemy import text

stmt = text("SELECT user_uuid FROM users").execution_options(
    query_formats={"UUID": "string"}
)

with engine.connect() as conn:
    rows = conn.execute(stmt).all()
```

<h3 id="sqlalchemy-error-handling">
  معالجة الأخطاء
</h3>

تستخدم الأخطاء التي يُطلقها المشغّل عبر اتصال SQLAlchemy أصنافَ DB-API المُصدَّرة من `clickhouse_connect.dbapi`. وهذه الأصناف هي كائنات الأصناف نفسها المقابلة لها في `clickhouse_connect.driver.exceptions`، ولذلك يغلّفها SQLAlchemy في الفئة الفرعية المطابقة من `sqlalchemy.exc.DBAPIError`. أما `StreamFailureError` فهو من نوع `OperationalError`، ويُغلَّف على هيئة `sqlalchemy.exc.OperationalError`.

إذا كان الإلغاء من جهة المستدعي قد يقاطع استدعاءً صريحًا لـ `AsyncConnection.invalidate()`، فشغّل عملية الإبطال ضمن مهمة تملكها، وانتظر اكتمالها قبل تمرير الإلغاء. يتيح ذلك لـ SQLAlchemy إكمال عمليات تتبّع سجلات الاتصال:

```python theme={null}
import asyncio


async def invalidate_safely(connection):
    invalidate_task = asyncio.create_task(connection.invalidate())
    cancellation = None
    while not invalidate_task.done():
        try:
            await asyncio.wait({invalidate_task})
        except asyncio.CancelledError as ex:
            cancellation = ex
    if cancellation is not None:
        try:
            invalidate_task.result()
        finally:
            raise cancellation
    invalidate_task.result()
```

لا تستخدم الاتصال ما دامت مهمة إبطاله قيد التشغيل. إذا أُلغي استدعاء مباشر لـ `await connection.invalidate()` وظلت قيمة `connection.invalidated` هي false، فاستدعِ `connection.invalidate()` مجددًا مع await لإكمال عملية التنظيف قبل استخدام الاتصال أو إغلاقه.

<h3 id="sqlalchemy-server-side-parameters">
  معلمات من جهة الخادم
</h3>

يعرض SQLAlchemy المعلمات عادةً من جهة العميل. فعِّل معلمات ClickHouse من جهة الخادم عند إنشاء المحرّك:

```python theme={null}
engine = create_engine(
    "clickhousedb://user:password@host:8123/mydb",
    server_side_params=True,
)
```

استخدم الوسيط نفسه `server_side_params=True` مع `create_async_engine()` للّهجة غير المتزامنة.

في هذا الوضع، يجب أن تكون كل قيمة مقيّدة من نوع SQLAlchemy متوافق مع ClickHouse. وتتحول قوائم `IN` المدعومة إلى معلمات ClickHouse من النوع `Array`. ويرفع المصرّف `CompileError` عندما يتعذر عليه استنتاج نوع متوافق أو معالجة قيمة مقيّدة بأمان.

يجب أن تكون أسماء الربط أسماء ClickHouse من نوع ASCII BareWord. تُرفض الأسماء التي تبدأ وتنتهي بـ `$` لأن المشغّل الأساسي يحجزها لمعلمات الاستعلام الثنائية الخام.

<h2 id="sqlalchemy-core-queries">
  استعلامات Core
</h2>

تدعم هذه اللهجة استعلامات `SELECT` في SQLAlchemy Core مع عمليات الربط، وعوامل التصفية، والترتيب، والحدود والإزاحات، و`DISTINCT`، وعمليات select المركبة.

تُترجم `union()` و`intersect()` و`except_()` في SQLAlchemy إلى `UNION DISTINCT` و`INTERSECT DISTINCT` و`EXCEPT DISTINCT` في ClickHouse. وتُترجم نظائرها `union_all()` و`intersect_all()` و`except_all()` إلى عوامل التشغيل المقابلة مع `ALL`. يحافظ هذا التعيين الصريح على دلالات التكرارات في SQLAlchemy بغض النظر عن الإعدادات الافتراضية لعمليات المجموعات في ClickHouse.

```python theme={null}
from sqlalchemy import MetaData, Table, select

metadata = MetaData(schema="mydb")
users = Table("users", metadata, autoload_with=engine)
orders = Table("orders", metadata, autoload_with=engine)
events = Table("events", metadata, autoload_with=engine)

stmt = (
    select(users.c.name, orders.c.product)
    .select_from(users.join(orders, users.c.id == orders.c.user_id))
    .order_by(users.c.name)
    .limit(10)
)

with engine.connect() as conn:
    rows = conn.execute(stmt).all()
```

يُدعَم `DELETE` الخفيف ويتطلب عبارة `WHERE` صريحة:

```python theme={null}
from sqlalchemy import delete

stmt = delete(users).where(users.c.name.like("%temporary%"))
with engine.connect() as conn:
    conn.execute(stmt)
```

<h3 id="sqlalchemy-literal-rendering">
  عرض القيم الحرفية
</h3>

عندما يضمّن SQLAlchemy قيمة مقيّدة باستخدام `literal_binds` أو `literal_execute`، تستخدم اللهجة أسلوب الاقتباس في ClickHouse لأنواع السلاسل النصية العامة وأنواع ClickHouse. وينطبق ذلك أيضًا عند استخدام أغلفة `TypeDecorator` واختيارات `with_variant()`. تحتفظ قيم السلاسل النصية بعلامات النسبة المئوية والشرطات المائلة العكسية، حتى مع وجود مَعلَمات مقيّدة أخرى.

تحتفظ قيم `datetime` في بايثون المقترنة بنوع SQLAlchemy ‏`DateTime64` الخاص بـ ClickHouse بأجزاء الميكروثانية، سواء في المعلمات على جهة العميل أو في القيم الحرفية المضمّنة، بما في ذلك القيم القابلة للإلغاء (nullable) والقيم المتداخلة داخل المصفوفات والصفوف (tuples). ويطبّق ClickHouse الدقة المُعلَنة، علمًا بأن قيم `datetime` في بايثون توفّر ما يصل إلى ستة أرقام كسرية. أما قيم `DateTime` العادية فتحتفظ بتنسيق الثواني الكاملة. وفي حالة عبارة `text()`، حدِّد النوع صراحةً باستخدام `bindparam("ts", type_=DateTime64(6))` للحفاظ على أجزاء الثانية.

يجب أن تتطابق أنواع الأعمدة في SQLAlchemy مع المخطط على الخادم؛ إذ إن تعريف `DateTime64` لعمود من النوع `DateTime` على الخادم يؤدي إلى عرض أجزاء الثانية، وقد يتسبب في أخطاء تحويل عند الإدراج وفي مقارنات `IN`.

في SQLAlchemy 2.x، تتطلب القيم الحرفية المضمّنة لأنواع `sqlalchemy.ARRAY` العامة التي تحتوي على عناصر `Tuple` من ClickHouse تعيين `dimensions=1`، أو عدد الأبعاد الأعلى المناسب في حالة المصفوفات المتداخلة، كي يعامل SQLAlchemy كل صف (tuple) كعنصر واحد. ولا يدعم SQLAlchemy 1.4 القيم الحرفية المضمّنة لأنواع `ARRAY` العامة.

إذا أُعيد استخدام معلمة تاريخ ووقت مُسمّاة، فيجب أن يكون لكل موضع تظهر فيه نوع ربط `DateTime64` متوافق للحفاظ على أجزاء الثانية؛ فأي موضع غير محدد النوع أو ذي نوع متعارض يُبقي على تنسيق الثواني الكاملة. عيِّن `type_=DateTime64(6)` في كل `bindparam`، أو استخدم أسماء معلمات مختلفة بالأنواع المناسبة.

<h3 id="sqlalchemy-json-type-hints">
  تلميحات نوع JSON
</h3>

أعلن عن مسارات JSON ذات الأنواع المحددة باستخدام تعيين `typed_paths`. يمكن أن يكون نوع المسار صنف نوع من ClickHouse SQLAlchemy، أو نسخة مهيَّأة، أو سلسلة نصية تحمل اسم نوع ClickHouse. وتدعم سلاسل أسماء الأنواع الأنواعَ التي لا يوجد لها مُنشئ في SQLAlchemy، مثل `Dynamic`، كما يمكن استخدامها لتعبيرات الأنواع المهيَّأة المعقدة، وهي تحافظ على الأسماء داخل `Tuple` المسمّى.

يمكن أن تحتوي سلاسل أسماء الأنواع على أنواع JSON متداخلة مهيَّأة مثل ``Array(JSON(`child` UInt32))``. وأسماء أنواع ClickHouse المعروفة غير حساسة لحالة الأحرف داخل هذه السلاسل، وتُخرَج بحالة الأحرف القياسية الخاصة بها. ويجب أن تحتوي السلسلة على تعبير نوع واحد كامل، إذ يُرفض أي نص لاحق أو وسائط JSON متداخلة غير صحيحة التكوين.

لا يُدعم `Tuple()` الفارغ كمسار JSON ذي نوع محدد، لأن ClickHouse لا يستطيع تسلسله عبر تنسيق Native الخاص بـ JSON column. ويدعم المشغّل الأساسي `Tuple()` في أعمدة الاستعلام والإدراج في أي موضع، بما في ذلك التداخل داخل الصفوف الموضعية أو المسمّاة، وداخل `Array`، وكذلك بصيغة `Nullable(Tuple())` حيثما كان ذلك مفعّلًا في الخادم.

```python theme={null}
from sqlalchemy import Column, MetaData, Table

from clickhouse_connect.cc_sqlalchemy.datatypes.sqltypes import JSON, UInt32

events = Table(
    "events",
    MetaData(),
    Column(
        "payload",
        JSON(
            typed_paths={
                "event.id": UInt32,
                "details": "Tuple(id UInt32, label Nullable(String))",
                "attributes": "Variant(String, Array(String))",
            },
            max_dynamic_paths=256,
            max_dynamic_types=16,
            skip_paths=["internal.debug"],
            skip_regexps=[r"^private\."],
        ),
    ),
)
```

بالنسبة لمسارات معرّفات بايثون البسيطة، تُعد وسيطات الكلمات المفتاحية اختصارًا لـ `typed_paths`، على سبيل المثال `JSON(user_id=UInt32)`. استخدم `typed_paths` مع المسارات التي تحتوي على نقاط أو مسافات أو backticks أو نقاطًا مُرمَّزة بصيغة `%2E`، أو الأسماء التي تتطابق مع خيارات المُنشئ. ويُدعم مسار مُحدَّد النوع باسم `SKIP` من خلال الربط. المفاتيح في `typed_paths` والقيم في `skip_paths` هي أسماء مفكوكة الترميز. وتُعامل الـ backticks وعلامات الاقتباس المزدوجة في بداية المسار أو نهايته بوصفها محارف حرفية ضمن المسار، لا بوصفها اقتباس SQL مطبّقًا مسبقًا. أما داخل سلسلة النوع الخام، فتمثّل الـ backticks وعلامات الاقتباس المزدوجة صياغة معرّفات ClickHouse.

يمكن تهيئة ما يصل إلى 1000 مسار مُحدَّد النوع. ويقبل `max_dynamic_paths` قيمًا من 0 إلى 10000، ويقبل `max_dynamic_types` قيمًا من 0 إلى 254. وتنطبق هذه النطاقات أيضًا داخل سلاسل نوع JSON المتداخلة الخام. وتُحذف القيم الافتراضية الصريحة للخادم البالغة 1024 و32 من الـ DDL المُولَّد. وتُزال التكرارات من مسارات التخطي البسيطة. ولا تتحقق بايثون من صحة سلاسل التعابير النمطية لأن ClickHouse يستخدم صياغة RE2، ويُحتفظ بالتعابير النمطية المكررة كما هي.

لا يمكن أن يحمل مسار تخطٍّ بسيط الاسم `REGEXP` تحديدًا، لأن ClickHouse يحجز هذا الـ token لصيغة `SKIP REGEXP`. أما أسماء مثل `REGEXP_foo` فتبقى صالحة. وفي سلسلة نوع JSON خام، يجب أن يكون معامل `SKIP` البسيط معرّف ClickHouse واحدًا أو معرّفًا مركبًا مفصولًا بنقاط. ولا يمكن أن يبدأ المعرّف المركب غير المقتبس بـ `REGEXP`؛ فاقتبس المكوّن الأول عندما يكون جزءًا من بيانات المسار. ويجب أن يتضمن `SKIP REGEXP` قيمة حرفية نصية واحدة بين علامتي اقتباس مفردتين. واقتبس أجزاء المعرّف باستخدام الـ backticks أو علامات الاقتباس المزدوجة عندما تحتوي على مسافات أو علامات ترقيم. وتدعم تلميحات نوع JSON الخام `Variant(...)`؛ أما `Variant` بمفرده فليس له مُنشئ عام في SQLAlchemy. وتُرتَّب أعضاء `Variant` وتُزال تكراراتها وفق الأسماء القياسية نفسها التي يستخدمها ClickHouse.

ويرتّب المُنشئ الوسائط بالصيغة القياسية نفسها التي يعيدها ClickHouse. كما تحافظ الأنواع المنعكسة، ونسخ أنواع SQLAlchemy، والتوليد التلقائي في Alembic على التهيئة.

<h3 id="sqlalchemy-json-subcolumns">
  الأعمدة الفرعية لـ JSON
</h3>

بالنسبة إلى عمود مُعرَّف أو ممثَّل في ClickHouse بصفته `JSON`، استخدم الأقواس المربعة لاختيار مقطع واحد في كل مرة من مسار عمود فرعي مدعوم بالتخزين:

```python theme={null}
from sqlalchemy import Column, MetaData, Table, select

from clickhouse_connect.cc_sqlalchemy.datatypes.sqltypes import JSON, UInt32

events = Table(
    "events",
    MetaData(),
    Column("payload", JSON),
)

request_id = events.c.payload["context"]["request"].subcolumn(
    "id",
    type_=UInt32,
)

stmt = select(
    events.c.payload["severity"].label("severity"),
    request_id.label("request_id"),
)
```

يُحوَّل `payload["severity"]` إلى صياغة المعرّف المنقّط في ClickHouse. يُقتبس كل جزء على حدة، على سبيل المثال `` `events`.`payload`.`severity` ``. يقرأ العمود الفرعي JSON المخزّن في ClickHouse ولا يستدعي `getSubcolumn`. استخدم `[]` أو `.subcolumn()` بشكل متسلسل، مرة واحدة لكل مقطع من المسار. يجب أن يكون كل مقطع سلسلة غير فارغة.

يؤدي تمرير `type_` إلى `.subcolumn()` إلى تغليف المسار المنقّط بعملية `CAST` في SQL وإسناد هذا النوع إلى تعبير SQLAlchemy. من دون `type_`، تتصرف `.subcolumn("segment")` مثل `["segment"]`.

يكون نوع المسار غير المحدد `Dynamic` في ClickHouse. لا يسمح ClickHouse باستخدام قيم `Dynamic` مباشرةً في `ORDER BY` أو `GROUP BY`. مرّر `type_` عند استخدام عمود فرعي في هذه المواضع.

بالنسبة إلى الشيفرة ذات الأنواع الثابتة، استورد `json_subcolumn` من `clickhouse_connect.cc_sqlalchemy`. تقبل الدالة المساعدة أيضًا مقطعًا واحدًا في كل مرة وتحافظ على نوع نتيجة بايثون المحدد بواسطة `type_`:

```python theme={null}
from clickhouse_connect.cc_sqlalchemy import json_subcolumn

context = json_subcolumn(events.c.payload, "context")
request = json_subcolumn(context, "request")
request_id = json_subcolumn(request, "id", type_=UInt32)
```

في هذا المثال، تتعامل أدوات التحقق من الأنواع مع `request_id` على أنه `ColumnElement[int]`.

يُقتبس كل مقطع بشكل مستقل، بما في ذلك الأسماء التي تحتوي على مسافات أو علامات اقتباس خلفية. لا تجعل علامات الاقتباس الخلفية النقطة قيمة حرفية في معالجة مسارات JSON في ClickHouse. عند تمكين `json_type_escape_dots_in_keys`، استخدم ترميز ClickHouse `%2E` للنقاط الحرفية في المفاتيح. للوصول إلى مفتاح باسم `a.b`، استخدم `payload["a%2Eb"]`، وليس `payload["a.b"]`.

<h3 id="sqlalchemy-query-extensions">
  امتدادات استعلام ClickHouse
</h3>

استورد `select` من `clickhouse_connect.cc_sqlalchemy` لتمكين أدوات التحقق الساكنة من الأنواع من التعرّف على طرائق ClickHouse المعرّفة الأنواع. كما تتوفر هذه الطرائق أيضًا في `sqlalchemy.select` القياسي وقت التشغيل.

```python theme={null}
from clickhouse_connect.cc_sqlalchemy import select

stmt = (
    select(events.c.user_id, events.c.event_type)
    .final()
    .prewhere(events.c.event_date >= "2026-01-01")
    .sample(0.1)
    .limit_by([events.c.user_id], 3)
)
```

طرق `Select` في ClickHouse هي:

| الطريقة | ميزة SQL |
| - | - |
| `.final()` | `FINAL` لجدول |
| `.sample(value)` | `SAMPLE`، باستخدام نسبة أو عدد صفوف أو تعبير |
| `.prewhere(expression)` | `PREWHERE`؛ تُجمَع الاستدعاءات المتكررة باستخدام `AND` |
| `.limit_by(columns, limit, offset=None)` | `LIMIT ... BY` |
| `.array_join(...)` | `ARRAY JOIN` |
| `.left_array_join(...)` | `LEFT ARRAY JOIN` |
| `.ch_join(...)` | عمليات `JOIN` في ClickHouse مع خيارات `strictness` و`distribution` و`using` و`cross` |
| `.cte(name, materialized=True)` | `WITH name AS MATERIALIZED (...)` |

تُعد `Select.with_hint()` في SQLAlchemy واجهة برمجة تطبيقات لتلميحات الجداول. لا تُنشئ لهجة ClickHouse تلميحات الجداول. يؤدي استخدام تلميح wildcard أو `clickhousedb` قابل للتطبيق إلى إصدار `SAWarning` مع إبقاء SQL المُولَّد دون تغيير. استخدم `final()` أو `sample()` أو `prewhere()` أو `limit_by()` لبنود ClickHouse هذه.

تُعد `Select.with_statement_hint()` واجهة برمجة تطبيقات لتوجيه خام يُضاف في النهاية. وهي تُلحق النص المقدَّم بنهاية `SELECT` دون تحقق خاص بـ ClickHouse. يظل ذلك متاحًا لاستخدامه مع SQL ثابت وموثوق، مثل `SETTINGS max_threads=1`:

```python theme={null}
stmt = select(events.c.id).with_statement_hint("SETTINGS max_threads=1")
```

بالنسبة إلى إعدادات ClickHouse، يُفضَّل استخدام خيارات التنفيذ كي يتعامل المشغّل معها بشكل منفصل عن نص SQL:

```python theme={null}
stmt = select(events.c.id).execution_options(settings={"max_threads": 1})
```

على سبيل المثال، يمكن ربط `GLOBAL ANY LEFT JOIN` في ClickHouse دون الحاجة إلى تداخل `FromClause` مخصّص:

```python theme={null}
stmt = (
    select(events.c.id, users.c.name)
    .select_from(events)
    .ch_join(
        users,
        events.c.user_id == users.c.id,
        isouter=True,
        strictness="ANY",
        distribution="GLOBAL",
    )
)
```

استخدم الصيغة الصريحة `Lambda` مع الدوال عالية الرتبة في ClickHouse:

```python theme={null}
from sqlalchemy import column, func

from clickhouse_connect.cc_sqlalchemy import Lambda, select

stmt = select(
    func.arrayMap(
        Lambda("x", column("x") * 2),
        events.c.metrics,
    ).label("doubled")
)
```

تُحوَّل بنية SQLAlchemy القياسية `values()` عند الترجمة إلى صياغة دالة الجدول `VALUES` في ClickHouse، بما في ذلك عند استخدامها في تعبير الجدول الشائع. يتطلب شكل تعبير الجدول الشائع استخدام SQLAlchemy 2.0.42 أو إصدار أحدث، إذ أُضيفت `Values.cte()`.

<h3 id="sqlalchemy-materialized-ctes">
  تعبيرات الجدول الشائعة المُجسَّدة
</h3>

يُضمّن ClickHouse تعبير الجدول الشائع تلقائيًا، لذا إذا أُشير إلى CTE أكثر من مرة، يُنفَّذ محتواه مرةً لكل مرجع. مرّر `materialized=True` إلى `.cte()` لإنتاج `WITH <name> AS MATERIALIZED (...)`، بحيث يُحسب المحتوى مرةً واحدة:

```python theme={null}
from sqlalchemy import func

from clickhouse_connect.cc_sqlalchemy import select

ranked = (
    select(book.c.book_id, func.row_number().over(order_by=book.c.score.desc()).label("result_rank"))
    .where(book.c.genre == "sci-fi")
    .order_by(book.c.score.desc())
    .limit(100)
    .cte("ranked", materialized=True)
)

stmt = (
    select(book.c.book_id, ranked.c.result_rank)
    .select_from(book)
    .ch_join(ranked, book.c.book_id == ranked.c.book_id, strictness="ANY")
    .where(book.c.book_id.in_(select(ranked.c.book_id)))
    .execution_options(settings={"enable_materialized_cte": 1, "enable_analyzer": 1})
)
```

لا يُجسِّد الخادم تعبير الجدول الشائع إلا عند وجود الكلمة المفتاحية وتعيين `enable_materialized_cte=1` وتمكين المحلِّل. عيّن `enable_materialized_cte` على التعليمة أو الاتصال أو المحرك كما هو موضح في [إعدادات خاصة بكل استعلام](#sqlalchemy-per-query-settings). يكون المحلِّل مُمكّنًا افتراضيًا على كل خادم يدعم هذه الميزة، لذا يُعد تعيين `enable_analyzer=1` صراحةً إجراءً احترازيًا. يُعد `enable_materialized_cte` إعدادًا تجريبيًا في ClickHouse. عند استخدام `enable_materialized_cte=0` أو `enable_analyzer=0`، ينجح الاستعلام ويُرجع الصفوف نفسها. يتجاهل ClickHouse قيمة `MATERIALIZED` بصمت ويضمّن تعبير الجدول الشائع مجددًا، لذا فإن نسيان الإعداد يؤثر في الأداء دون ظهور أي تنبيه. تتطلب تعبيرات الجدول الشائع المُجسَّدة ClickHouse 26.3 أو إصدارًا أحدث. ترفض الخوادم الأقدم الكلمة المفتاحية باعتبارها خطأً نحويًا.

بالنسبة إلى تعليمة مُنشأة باستخدام `sqlalchemy.select` القياسي، استخدم `cte()` على مستوى الوحدة بدلًا من ذلك. تأخذ التعليمة كوسيط أول، وتكافئ `Select.cte()` فيما عدا ذلك:

```python theme={null}
from sqlalchemy import select as sa_select

from clickhouse_connect.cc_sqlalchemy import cte

ranked = cte(sa_select(book.c.book_id), "ranked", materialized=True)
```

لا تظهر الكلمة المحجوزة إلا في لهجة ClickHouse، لذا يُترجم التعليمة المشتركة مع backend آخر فيها دون تغيير.

لا يدعم ClickHouse تعبيرات الجدول الشائع المادية التعاودية. تثير helpers الخاصة بـ SQLAlchemy الخطأ `ValueError` عند تعيين كلٍّ من `recursive=True` و`materialized=True`.

<h2 id="sqlalchemy-ddl-reflection">
  DDL واستكشاف البنية
</h2>

يوفّر ClickHouse Connect أنواع بيانات ClickHouse، ومحركات الجداول، وبُنى القواميس، وDDL لقواعد البيانات، واستكشاف بنية الجداول.

تُستكشف أعمدة `Variant` المستقلة (standalone) عبر نوع SQLAlchemy داخلي، ويحافظ التوليد التلقائي في Alembic على أسماء أنواعها الخام القياسية دون تغييرات متكررة في الأنواع. أما أعمدة `Geometry` و`MultiPoint` فتُستكشف كأنواع SQLAlchemy عامة.

```python theme={null}
import sqlalchemy as db
from sqlalchemy import MetaData

from clickhouse_connect.cc_sqlalchemy.datatypes.sqltypes import DateTime64, String, UInt32
from clickhouse_connect.cc_sqlalchemy.ddl.custom import CreateDatabase
from clickhouse_connect.cc_sqlalchemy.ddl.tableengine import MergeTree

with engine.connect() as conn:
    conn.execute(CreateDatabase("example_db", exists_ok=True))

    metadata = MetaData(schema="example_db")
    events = db.Table(
        "events",
        metadata,
        db.Column("id", UInt32, primary_key=True),
        db.Column("user", String),
        db.Column("created_at", DateTime64(3)),
        MergeTree(order_by="id"),
    )
    events.create(conn)

    reflected = db.Table("events", MetaData(schema="example_db"), autoload_with=conn)
    assert reflected.engine is not None
```

تحمل الأعمدة المسترجعة `server_default` لتعبيرات `DEFAULT`، وسمات خاصة بكل dialect مثل `clickhouse_codec` و`clickhouse_ttl` و`clickhouse_materialized` و`clickhouse_alias` إن وُجدت.

تستخدم قيم السلاسل النصية في عبارات `DEFAULT` و`MATERIALIZED` و`ALIAS` و`TTL` إفلات السلاسل النصية في ClickHouse. وينطبق الإفلات نفسه على تعليقات الجداول والقواميس والأعمدة، بما في ذلك التعليقات التي يصدرها Alembic.

تقبل وسائط مفاتيح MergeTree مثل `order_by` و`partition_by` و`primary_key` و`sample_by` و`ttl` أعمدة SQLAlchemy وتعبيرات SQL، بالإضافة إلى السلاسل النصية العادية.

يمكن استدعاء `Memory()` و`Log()` و`StripeLog()` و`TinyLog()` و`Null()` و`Set()` دون أي وسائط، وتبقى بنيتها سليمة عند كتابتها ثم قراءتها عبر التوليد التلقائي في Alembic. ولا تزال وسيطة القاموس الحالية مدعومة. استخدم `settings={...}` لتمرير إعدادات المحرك.

يقبل `SummingMergeTree` و`ReplicatedSummingMergeTree` وسيطة `columns` اختيارية لا تُمرَّر إلا بالاسم. وتحتفظ الوسائط الموضعية الحالية بمعناها، لذا لا يزال `SummingMergeTree("id")` يضبط `ORDER BY id`.

```python theme={null}
from clickhouse_connect.cc_sqlalchemy.ddl.tableengine import SummingMergeTree

engine_clause = SummingMergeTree("id", columns=("delta", "n_tx"))
# Sum delta and n_tx for rows with the same id.
```

مرّر سلسلة نصية، أو عمود SQLAlchemy، أو سمة عمود مُعيَّن (mapped)، أو قائمة أو صفًّا (tuple) غير فارغ من هذه القيم. تُحاط عناصر السلاسل النصية في القوائم والصفوف بعلامات الاقتباس بوصفها معرّفات. أما السلسلة النصية المفردة فتُمرَّر بوصفها SQL خامًا، مثل `"delta"` أو `"(delta, n_tx)"`. ويتطلب الخادم معرّفات لهذه الأعمدة. احذف `columns` لتترك لـ ClickHouse تحديد الأعمدة المراد جمعها. ويحافظ استكشاف البنية والتوليد التلقائي في Alembic على قائمة الأعمدة المحددة صراحةً.

<h2 id="sqlalchemy-inserts">
  عمليات الإدراج واستخدام ORM الأساسي
</h2>

عمليات إدراج Core ونماذج ORM البسيطة مدعومة. في اللهجة المتزامنة، يُفضَّل استخدام عمليات إدراج Core عبر executemany لمسارات البيانات المجمّعة المتوافقة. أما عمليات الإدراج المجمّعة غير المتزامنة، فاستخدم لها المسار الأصلي `AsyncClient.insert()` الموضّح في [الاتصالات غير المتزامنة](#sqlalchemy-async-connections).

```python theme={null}
with engine.connect() as conn:
    conn.execute(
        events.insert(),
        [
            {"id": 13, "user": "user_1"},
            {"id": 79, "user": "user_2"},
        ],
    )
```

بالنسبة إلى اللهجة المتزامنة، تستخدم عمليات إدراج `executemany` البسيطة في Core التي يولّدها مصرّف SQLAlchemy عملية إدراج مجمّعة Native واحدة. أما `executemany` غير المتزامن فيرسل طلبًا واحدًا لكل مجموعة معلمات، كما هو موضّح في [الاتصالات غير المتزامنة](#sqlalchemy-async-connections). أما استعلامات Raw SQL وعمليات الإدراج التي تتضمن تعابير أو دلالات أخرى يتعذّر توجيهها بأمان، فتحتفظ بعبارة SQL الأصلية وتُنفَّذ مرة واحدة لكل مجموعة معلمات. وإذا فشلت مجموعة معلمات لاحقة، تبقى الصفوف التي كتبتها مجموعات المعلمات السابقة مثبّتة.

تعمل عبارات `insert(events).values([...])` الصريحة متعددة الصفوف مع صفوف القواميس، والصفوف من نوع tuple المرتّبة وفق ترتيب أعمدة الجدول، وتعابير SQL الخاصة بكل صف. ويستخدم Pandas `to_sql(method="multi")` هذا الشكل؛ إذ يُدرج الصفوف لكنه يُرجع `0`، لأن عبارات INSERT النصية تُبلغ عن عدد صفوف يساوي `0` عبر مؤشر DB-API. ويحدد SQLAlchemy قائمة الأعمدة استنادًا إلى الصف الأول، فتُتجاهَل مفاتيح القاموس الإضافية في الصفوف اللاحقة وقيم tuple الواقعة خارج قائمة الأعمدة المحددة. وإذا خلا صف لاحق من إحدى القيم المحددة، يفشل التصريف. لذا احرص على أن تتضمن جميع الصفوف الأعمدة نفسها.

مع حدود نموذج HTTP الافتراضية في ClickHouse 26.4 والإصدارات الأحدث، لا يصلح `server_side_params=True` إلا للدفعات الصريحة الصغيرة، أي أقل من 1000 قيمة ربط تقريبًا مع ترك هامش للحقول الأخرى. ويمكن رفع هذا الحد الأقصى عبر تهيئة الخادم. أما الدفعات البسيطة الكبيرة مع اللهجة المتزامنة، فمرّر الصفوف بوصفها الوسيط الثاني إلى `execute()` كي يتمكن المشغّل من استخدام مسار الإدراج المجمّع Native الخاص به. وللبيانات المجمّعة غير المتزامنة، استخدم await مع الطريقة الأصلية `AsyncClient.insert()`.

```python theme={null}
import sqlalchemy as db
from sqlalchemy import MetaData
from sqlalchemy.orm import Session, declarative_base

from clickhouse_connect.cc_sqlalchemy.datatypes.sqltypes import String, UInt32
from clickhouse_connect.cc_sqlalchemy.ddl.tableengine import MergeTree

Base = declarative_base(metadata=MetaData(schema="example_db"))


class User(Base):
    __tablename__ = "users"
    __table_args__ = (MergeTree(order_by=["id"]),)

    id = db.Column(UInt32, primary_key=True)
    name = db.Column(String)


Base.metadata.create_all(engine)

with Session(engine) as session:
    session.add(User(id=13, name="user_1"))
    session.bulk_save_objects([User(id=79, name="user_2")])
    session.commit()
```

<h2 id="sqlalchemy-alembic">
  ترحيلات Alembic
</h2>

يتضمن ClickHouse Connect تكاملًا مع Alembic لإجراء ترحيلات مخطط ClickHouse. ثبّته باستخدام:

```bash theme={null}
pip install "clickhouse-connect[alembic]"
```

لإجراء الترحيلات عبر اللهجة غير المتزامنة، ثبّت كلتا الحزمتين الإضافيتين:

```bash theme={null}
pip install "clickhouse-connect[alembic,sqlalchemy-async]"
```

أنشئ مشروع Alembic غير متزامن، ثم استبدل البيئة التي وُلِّدت فيه بالمثال المتوافق مع ClickHouse:

```bash theme={null}
alembic init -t async alembic
```

يستخدم ملف `alembic.ini` المُولَّد الإعداد `script_location = %(here)s/alembic`. أبقِ على هذا الإعداد إذا كان اسم دليل الترحيل `alembic`، وإلا فحدّثه ليشير إلى الدليل الذي مرّرته إلى `alembic init`. استبدل `alembic/env.py` بـ [مثال `env.py` غير المتزامن لـ Alembic](https://github.com/ClickHouse/clickhouse-connect/blob/main/examples/alembic_async/env.py) المُضمَّن في المستودع، ثم اضبط `sqlalchemy.url` في `alembic.ini`.

استورد `clickhouse_connect.cc_sqlalchemy.alembic` في ملف `env.py` الخاص بـ Alembic لتسجيل تكامل اللهجة. يدعم التوليد التلقائي تغييرات الجداول الشائعة، بما في ذلك إنشاء الجداول وإزالتها، وإضافة الأعمدة وتعديلها وحذفها، والقيم الافتراضية، والتعليقات. استخدم العمليات اليدوية لإعادة تسمية الجداول والأعمدة. راجع كل عملية ترحيل مولَّدة قبل تطبيقها.

تظل دوال الترحيل في Alembic متزامنة. إذ تُنشئ البيئة غير المتزامنة كائن `AsyncEngine`، وتفتح `AsyncConnection`، ثم تمرّر دالة الترحيل المتزامنة إلى `await connection.run_sync(...)`. أما الترحيلات دون اتصال فتستدعي `context.configure(url=..., literal_binds=True, dialect_opts={"paramstyle": "named"})` مباشرةً دون إنشاء محرك. ويتضمن [مثال `env.py` غير المتزامن لـ Alembic](https://github.com/ClickHouse/clickhouse-connect/blob/main/examples/alembic_async/env.py) المُضمَّن في المستودع كلا المسارين، ويقرأ URL الاتصال من تهيئة `sqlalchemy.url` القياسية في Alembic. كما يحتفظ بخطافات Alembic وخياراتها الخاصة بـ ClickHouse الواردة في المثال العملي، بما في ذلك `include_object` و`make_include_name(...)` و`clickhouse_writer` و`version_table`. لا تستخدم `engine.sync_engine` لتشغيل الترحيلات غير المتزامنة أو التخلص منها.

تشمل أدوات `op.*` المساعدة الخاصة بـ ClickHouse ما يلي:

* فهارس تخطي البيانات، بما في ذلك عمليات الإضافة وmaterialize والحذف.
* الإسقاطات، بما في ذلك عمليات الإضافة وmaterialize والحذف.
* تعديل إعدادات جدول MergeTree وإعادة ضبطها.
* إنشاء materialized view وإزالتها.
* إنشاء القواميس وإزالتها وإعادة تحميلها.

فهارس تخطي البيانات في ClickHouse ليست فهارس SQLAlchemy. يتم رفض `Index` و`Column(index=True)` و`op.create_index` و`op.drop_index` لتجنّب عبارات DDL الجزئية أو غير الصحيحة. استخدم `op.add_clickhouse_index` و`op.drop_clickhouse_index`.

راجع [المثال العملي الكامل لـ Alembic](https://github.com/ClickHouse/clickhouse-connect/blob/main/clickhouse_connect/cc_sqlalchemy/alembic/WORKED_EXAMPLE.md). كما ينبغي للمستخدمين الذين يرحّلون من `clickhouse-sqlalchemy` قراءة [دليل الترحيل](https://github.com/ClickHouse/clickhouse-connect/blob/main/clickhouse_connect/cc_sqlalchemy/MIGRATING_FROM_CLICKHOUSE_SQLALCHEMY.md).

<h2 id="scope-and-limitations">
  النطاق والقيود
</h2>

* لا يوفّر ClickHouse المعاملات التقليدية عبر لهجة HTTP هذه. ينظّم `engine.begin()` و`Session.commit()` العمل على جانب بايثون، لكن commit و التراجع لا يُحدثان أي تأثير على الخادوم.
* لا تدعم هذه اللهجة `UPDATE`، والمعاملات ثنائية الطور، والتسلسلات، و`RETURNING`، ومستويات العزل المتقدمة. استخدم ClickHouse SQL الصريح لتنفيذ تعديلات الخادوم عند الحاجة.
* يوفّر `Column(..., primary_key=True)` هوية الكائن في SQLAlchemy، لكنه لا ينشئ قيد تفرد على جانب الخادوم. حدِّد تعبيرات الفرز وتعبيرات المفتاح الأساسي الاختيارية من خلال محرك الجدول.
* لا تتوفر البيانات الوصفية التقليدية للمفاتيح الخارجية وقيود التفرد والفهارس القياسية، لأن ClickHouse لا يفرض هذه القيود.
* تخرج إدارة العلاقات في ORM، وتحديثات وحدة العمل، والتتابعات، والتحميل الفوري أو المؤجل للعلاقات، عن نطاق ORM المدعوم.
