> ## 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.

> عميل C# الرسمي للاتصال بـ ClickHouse.

# عميل C# الرسمي لـ ClickHouse

export const Image = ({img, alt, size = "lg", background}) => {
  const normalizedSize = ["sm", "md", "lg"].includes(size) ? size : "lg";
  const backgroundColor = background === "white" ? "white" : background === "black" ? "rgb(31 31 28)" : undefined;
  return <div className={`ch-image-${normalizedSize}`}>
      <Frame>
        <img src={img} alt={alt} style={{
    backgroundColor
  }} />
      </Frame>
    </div>;
};

عميل C# الرسمي للاتصال بـ ClickHouse.
تتوفّر الشيفرة المصدرية للعميل في [مستودع GitHub](https://github.com/ClickHouse/clickhouse-cs).
طوّره في الأصل [Oleg V. Kozlyuk](https://github.com/DarkWanderer).

توفّر المكتبة واجهتَي برمجة تطبيقات رئيسيتين:

* **`ClickHouseClient`** (موصى به): عميل عالي المستوى وآمن للاستخدام من عدة خيوط، ومصمَّم للاستخدام بنمط singleton. يوفّر واجهة برمجة تطبيقات غير متزامنة وبسيطة للاستعلامات وعمليات الإدراج المجمّع. وهو الأنسب لمعظم التطبيقات.

* **ADO.NET** (`ClickHouseDataSource`, `ClickHouseConnection`, `ClickHouseCommand`): تجريدات قياسية لقواعد البيانات في .NET. وهي مطلوبة لتكامل ORM ‏(Dapper وLinq2db) وعندما تحتاج إلى التوافق مع ADO.NET. تُعد `ClickHouseBulkCopy` فئة مساعدة لإدراج البيانات بكفاءة باستخدام اتصال ADO.NET. الفئة `ClickHouseBulkCopy` مُهمَلة وستُزال في إصدار مستقبلي؛ استخدم `ClickHouseClient.InsertBinaryAsync` بدلاً منها.

تشترك كلتا الواجهتين في مجمّع اتصالات HTTP الأساسي نفسه، ويمكن استخدامهما معًا داخل التطبيق نفسه.

<h2 id="migration-guide">
  دليل الترحيل
</h2>

1. حدّث ملف `.csproj` لاستخدام اسم الحزمة الجديد `ClickHouse.Driver` و[أحدث إصدار على NuGet](https://www.nuget.org/packages/ClickHouse.Driver).
2. حدّث جميع مراجع `ClickHouse.Client` إلى `ClickHouse.Driver` في شيفرة مشروعك.

***

<h2 id="supported-net-versions">
  إصدارات ‎.NET‎ المدعومة
</h2>

يدعم `ClickHouse.Driver` إصدارات ‎.NET‎ التالية:

* .NET 6.0
* .NET 8.0
* .NET 9.0
* .NET 10.0

<h2 id="supported-clickhouse-versions">
  إصدارات ClickHouse المدعومة
</h2>

يدعم العميل رسميًا الإصدارات الثلاثة الأخيرة، بالإضافة إلى آخر إصدارين طويلَي الدعم (LTS).

<h2 id="installation">
  التثبيت
</h2>

ثبّت الحزمة من NuGet:

```bash theme={null}
dotnet add package ClickHouse.Driver
```

أو باستخدام مدير حزم NuGet:

```bash theme={null}
Install-Package ClickHouse.Driver
```

<h2 id="quick-start">
  البدء السريع
</h2>

```csharp theme={null}
using ClickHouse.Driver;

// Create a client (typically as a singleton)
using var client = new ClickHouseClient("Host=my.clickhouse;Protocol=https;Port=8443;Username=user");

// Execute a query
var version = await client.ExecuteScalarAsync("SELECT version()");
Console.WriteLine(version);
```

<h2 id="configuration">
  التهيئة
</h2>

هناك طريقتان لتهيئة اتصالك بـ ClickHouse:

* **سلسلة الاتصال:** أزواج مفتاح/قيمة مفصولة بفواصل منقوطة، تحدد المضيف وبيانات اعتماد المصادقة وخيارات الاتصال الأخرى.
* **كائن `ClickHouseClientSettings`:** كائن تهيئة مضبوط الأنواع يمكن تحميله من ملفات التهيئة أو تعيينه في الشيفرة.

فيما يلي قائمة كاملة بجميع الإعدادات وقيمها الافتراضية وتأثيراتها.

<h3 id="connection-settings">
  إعدادات الاتصال
</h3>

| الخاصية | النوع | الافتراضي | مفتاح سلسلة الاتصال | الوصف |
| - | - | - | - | - |
| المضيف | `string` | `"localhost"` | `Host` | اسم المضيف أو عنوان IP لخادم ClickHouse |
| المنفذ | `ushort` | 8123 (HTTP) / 8443 (HTTPS) | `Port` | رقم المنفذ؛ تُحدَّد القيمة الافتراضية حسب البروتوكول |
| اسم المستخدم | `string` | `"default"` | `Username` | اسم المستخدم للمصادقة |
| كلمة المرور | `string` | `""` | `Password` | كلمة المرور للمصادقة |
| قاعدة البيانات | `string` | `""` | `Database` | قاعدة البيانات الافتراضية؛ عند تركها فارغة، تُستخدَم القيمة الافتراضية للخادم أو المستخدم |
| البروتوكول | `string` | `"http"` | `Protocol` | بروتوكول الاتصال: `"http"` أو `"https"` |
| المسار | `string` | `null` | `Path` | مسار URL في سيناريوهات الوكيل العكسي (مثل `/clickhouse`) |
| المهلة | `TimeSpan` | دقيقتان | `Timeout` | مهلة العملية (تُخزَّن بالثواني في سلسلة الاتصال) |

<h3 id="data-format-serialization">
  تنسيق البيانات والتسلسل
</h3>

| الخاصية | النوع | الافتراضي | مفتاح سلسلة الاتصال | الوصف |
| - | - | - | - | - |
| UseCompression | `bool` | `true` | `Compression` | يتحكم في ضغط النقل في كلا الاتجاهين للاستعلام العادي: فهو يطلب من الخادم ضغط الاستجابة (`enable_http_compression`؛ راجع `AcceptEncoding` لمعرفة الكوديك، الذي يمكن لقيمة صريحة أن تطلبه حتى مع تعطيل هذا الخيار) **و** يضغط جسم الطلب بـ gzip — باستثناء حالة `UseFormDataParameters`، إذ يُرسَل جسمها متعدد الأجزاء دون ضغط دائمًا. أما الإدراجات الثنائية فلا تعتمد عليه إطلاقًا؛ إذ تستخدم `InsertOptions.Compressor` — راجع [ضغط الإدراج](#insert-compression) |
| AcceptEncoding | `string` | `null` | `AcceptEncoding` | ترويسة `Accept-Encoding` المُرسَلة مع كل طلب، وهي تحل محل الكوديكات التي يعلن عنها المشغّل افتراضيًا (`zstd, lz4, gzip, deflate`). ويُفكّ ترميز ما يستجيب به الخادم بشفافية. راجع [فك ضغط الاستجابة](#response-decompression) |
| UseCustomDecimals | `bool` | `true` | `UseCustomDecimals` | استخدم `ClickHouseDecimal` للدقة الاعتباطية؛ وإذا كانت القيمة `false`، فسيُستخدم `decimal` في .NET (بحد 128 بت) |
| ReadStringsAsByteArrays | `bool` | `false` | `ReadStringsAsByteArrays` | اقرأ أعمدة `String` و`FixedString` كمصفوفات `byte[]` بدلًا من `string`؛ وهذا مفيد للبيانات الثنائية |
| UseFormDataParameters | `bool` | `false` | `UseFormDataParameters` | أرسل المعلمات كبيانات نموذج بدلًا من سلسلة استعلام URL |
| ReadBufferSize | `int` | `65536` (64 KiB) | `ReadBufferSize` | الحجم بالبايت للمخزن المؤقت الذي يقرأ استجابات استعلامات HTTP. يستعير المشغّل المخزن المؤقت من مجمع مشترك ويعيده عند التخلص من القارئ، فهو ليس تخصيصًا جديدًا مع كل استعلام. زِدْه لتقليل إعادة تعبئة المخزن المؤقت مع مجموعات النتائج الكبيرة. ويحتفظ المشغّل بمخزن مؤقت واحد لكل قارئ متزامن، لذا يزداد استهلاك الذاكرة بزيادة حجم المخزن المؤقت وعدد القراء المتزامنين. راجع [المخازن المؤقتة](#perf-buffers). |
| ParameterTypeResolver | `IParameterTypeResolver` | `null` | — | مُحلِّل مخصص لتعيين نوع المعلمات بأسلوب `@`؛ راجع [تعيين نوع المعلمات المخصص](#parameter-type-mapping) |
| ParameterFormatter | `IParameterFormatter` | `null` | — | مُنسِّق مخصص لتسلسل قيم المعلمات؛ راجع [تنسيق قيمة المعلمات المخصص](#parameter-value-formatting) |
| ReadValueConverter | `IReadValueConverter` | `null` | — | مُحوِّل مخصص يُطبَّق على القيم التي يُرجعها قارئ البيانات؛ راجع [تحويل قيمة القراءة المخصص](#read-value-conversion) |
| JsonReadMode | `JsonReadMode` | `Binary` | `JsonReadMode` | كيفية إرجاع بيانات JSON: `Binary` (يعيد `JsonObject`) أو `String` (يعيد سلسلة JSON الخام) |
| JsonWriteMode | `JsonWriteMode` | `String` | `JsonWriteMode` | كيفية إرسال بيانات JSON: `String` (يُسلسِل عبر `JsonSerializer` ويقبل جميع المدخلات) أو `Binary` (كائنات POCO المسجَّلة فقط مع تلميحات النوع) |
| MapReadMode | `MapReadMode` | `Dictionary` | `MapReadMode` | كيفية إرجاع بيانات `Map(K, V)`: `Dictionary` (يعيد `Dictionary<K, V>`؛ والمفتاح المتكرر يحتفظ بآخر قيمة له فقط) أو `KeyValuePairs` (يعيد `List<KeyValuePair<K, V>>`، محتفظًا بكل زوج). راجع [نوع Map](#type-map-reading-map) |
| AllowDuplicateJsonKeys | `bool` | `false` | `AllowDuplicateJsonKeys` | كيفية قراءة صف `JSON` تحمل مساراته المتداخلة قيمة في كليهما. القيمة `false` تطلق استثناءً، لأن الاحتفاظ بإحدى القيمتين يعني إسقاط الأخرى؛ أما `true` فتحتفظ بالقيمة التي يحملها الصف أخيرًا. راجع [المسارات المتداخلة](#type-map-reading-json) |

<h3 id="session-management">
  إدارة الجلسات
</h3>

| Property | Type | Default | Connection String Key | Description |
| - | - | - | - | - |
| UseSession | `bool` | `false` | `UseSession` | تمكين الجلسات ذات الحالة؛ يجعل الطلبات تُنفَّذ تسلسليًا |
| SessionId | `string` | `null` | `SessionId` | معرّف الجلسة؛ يُنشئ GUID تلقائيًا إذا كانت القيمة `null` وكان `UseSession` يساوي `true` |

<Note>
  تؤدي العلامة `UseSession` إلى الاحتفاظ بجلسة الخادم، مما يتيح استخدام عبارات `SET` والجداول المؤقتة. ستُعاد تهيئة الجلسات بعد 60 ثانية من عدم النشاط (المهلة الافتراضية). ويمكن تمديد مدة الجلسة عبر تعيين إعدادات الجلسة باستخدام عبارات ClickHouse أو إعدادات الخادم.

  تتيح الفئة `ClickHouseConnection` عادةً التشغيل المتوازي (أي يمكن لعدة خيوط تنفيذ الاستعلامات بالتزامن). ومع ذلك، فإن تمكين العلامة `UseSession` يقيّد ذلك باستعلام نشط واحد فقط لكل اتصال في أي لحظة (وهذا قيد على جهة الخادم).
</Note>

<h3 id="security">
  الأمان
</h3>

| الخاصية | النوع | الافتراضي | مفتاح سلسلة الاتصال | الوصف |
| - | - | - | - | - |
| SkipServerCertificateValidation | `bool` | `false` | — | تجاوز التحقق من شهادة HTTPS؛ **غير مخصص للاستخدام في بيئة الإنتاج** |

<h3 id="http-client-configuration">
  تهيئة عميل HTTP
</h3>

| الخاصية | النوع | الافتراضي | مفتاح سلسلة الاتصال | الوصف |
| - | - | - | - | - |
| HttpClient | `HttpClient` | `null` | — | مثيل `HttpClient` مخصص ومُهيأ مسبقًا |
| HttpClientFactory | `IHttpClientFactory` | `null` | — | مصنع مخصص لإنشاء مثيلات `HttpClient` |
| HttpClientName | `string` | `null` | — | اسم يستخدِمه `HttpClientFactory` لإنشاء عميل معيّن |

<h3 id="logging-debugging">
  التسجيل وتصحيح الأخطاء
</h3>

| Property | Type | Default | Connection String Key | Description |
| - | - | - | - | - |
| LoggerFactory | `ILoggerFactory` | `null` | — | مصنع logger المستخدم للتسجيل التشخيصي |
| EnableDebugMode | `bool` | `false` | — | تمكين تتبّع الشبكة في .NET (يتطلب LoggerFactory مع ضبط المستوى على Trace)؛ **تأثير ملحوظ على الأداء** |

<h3 id="custom-settings-roles">
  الإعدادات المخصصة والأدوار
</h3>

| الخاصية | النوع | القيمة الافتراضية | مفتاح سلسلة الاتصال | الوصف |
| - | - | - | - | - |
| CustomSettings | `IDictionary<string, object>` | فارغ | البادئة `set_*` | إعدادات خادم ClickHouse، راجع الملاحظة أدناه |
| Roles | `IReadOnlyList<string>` | فارغ | `Roles` | أدوار ClickHouse مفصولة بفواصل (مثل `Roles=admin,reader`) |
| ApplicationInfo | `IReadOnlyDictionary<string, string>` | فارغ | — | وسوم حرة تُضاف إلى ترويسة HTTP ‏`User-Agent` لإسناد الاستعلامات إلى كل تطبيق على حدة. |

<Note>
  عند استخدام سلسلة اتصال لتعيين إعدادات مخصصة، استخدم البادئة `set_`، مثل "set\_max\_threads=4". أما عند استخدام كائن ClickHouseClientSettings، فلا تستخدم البادئة `set_`.

  للاطلاع على القائمة الكاملة بالإعدادات المتاحة، راجع [هنا](/ar/reference/settings/session-settings).
</Note>

***

<h3 id="connection-string-examples">
  أمثلة لسلسلة الاتصال
</h3>

<h4 id="basic-connection">
  اتصال بسيط
</h4>

```text theme={null}
Host=localhost;Port=8123;Username=default;Password=secret;Database=mydb
```

<h4 id="with-custom-clickhouse-settings">
  باستخدام إعدادات ClickHouse مخصّصة
</h4>

```text theme={null}
Host=localhost;set_max_threads=4;set_readonly=1;set_max_memory_usage=10000000000
```

***

<h3 id="query-options">
  QueryOptions
</h3>

يتيح لك `QueryOptions` تجاوز الإعدادات على مستوى العميل لكل استعلام على حدة. جميع الخصائص اختيارية، ولا تستبدل القيم الافتراضية للعميل إلا عند تحديدها.

| الخاصية | النوع | الوصف |
| - | - | - |
| QueryId | `string` | معرّف استعلام مخصّص للتتبّع في `system.query_log` أو للإلغاء |
| Database | `string` | تجاوز قاعدة البيانات الافتراضية لهذا الاستعلام |
| Roles | `IReadOnlyList<string>` | تجاوز أدوار العميل لهذا الاستعلام |
| CustomSettings | `IDictionary<string, object>` | إعدادات خادم ClickHouse لهذا الاستعلام (مثل `max_threads`) |
| CustomHeaders | `IDictionary<string, string>` | ترويسات HTTP إضافية لهذا الاستعلام |
| UseSession | `bool?` | تجاوز سلوك الجلسة لهذا الاستعلام |
| SessionId | `string` | معرّف الجلسة لهذا الاستعلام (يتطلب `UseSession = true`) |
| BearerToken | `string` | تجاوز رمز المصادقة لهذا الاستعلام |
| ParameterTypeResolver | `IParameterTypeResolver` | تجاوز المُحلِّل على مستوى العميل لتعيين نوع المعلَمات بنمط `@`؛ راجع [تعيين نوع المعلَمات المخصّص](#parameter-type-mapping) |
| ParameterFormatter | `IParameterFormatter` | تجاوز المُنسِّق على مستوى العميل لتسلسل قيم المعلَمات بنمط `@`؛ راجع [تنسيق قيمة المعلمات المخصص](#parameter-value-formatting) |
| ReadValueConverter | `IReadValueConverter` | تجاوز المُحوِّل على مستوى العميل المُطبَّق على القيم المُعادة بواسطة قارئ البيانات؛ راجع [تحويل قيمة القراءة المخصص](#read-value-conversion) |
| MaxExecutionTime | `TimeSpan?` | مهلة الاستعلام من جانب الخادم (تُمرَّر كإعداد `max_execution_time`)؛ يُلغي الخادم الاستعلام إذا تم تجاوزها |
| AcceptEncoding | `string` | تجاوز `Accept-Encoding` لكل استعلام على حدة (مثل `"br"` و`"identity"`)، ويأخذ الأسبقية على `ClickHouseClientSettings.AcceptEncoding`؛ كما يفرض أيضًا `enable_http_compression=1` على `URL`. راجع [ضغط النقل لكل استعلام](#per-query-accept-encoding). |

**مثال:**

```csharp theme={null}
var options = new QueryOptions
{
    QueryId = "report-2024-001",
    Database = "analytics",
    CustomSettings = new Dictionary<string, object>
    {
        { "max_threads", 4 },
        { "max_memory_usage", 10_000_000_000 }
    },
    MaxExecutionTime = TimeSpan.FromMinutes(5)
};

var reader = await client.ExecuteReaderAsync(
    "SELECT * FROM large_table",
    parameters: null,
    options: options
);
```

***

<h3 id="insert-options">
  InsertOptions
</h3>

يوسّع `InsertOptions` ‏`QueryOptions` بإعدادات خاصة بعمليات الإدراج المجمّع عبر `InsertBinaryAsync`.

| الخاصية | النوع | القيمة الافتراضية | الوصف |
| - | - | - | - |
| BatchSize | `int` | 100,000 | عدد الصفوف في كل دفعة |
| MaxDegreeOfParallelism | `int` | 1 | عدد عمليات رفع الدُفعات المتوازية |
| Format | `RowBinaryFormat` | `RowBinary` | التنسيق الثنائي: `RowBinary` أو `RowBinaryWithDefaults` |
| Compressor | `IClickHouseCompressor` | `ZstdCompressor.Default` | المرمّاز المطبَّق على جسم الإدراج (`Content-Encoding`). القيمة `null` ترسله دون ضغط. راجع [ضغط الإدراج](#insert-compression) |
| QueryPlacement | `InsertQueryPlacement` | `Body` | المكان الذي تُرسَل فيه تعليمة `INSERT INTO ... FORMAT ...`: ‏`Body` (قبل الصفوف) أو `Url` (كوسيط `query` في الـ URL). راجع [موضع استعلام الإدراج](#insert-query-placement) |
| ColumnTypes | `IReadOnlyDictionary<string, string>` | `null` | اسم العمود ← سلسلة النوع في ClickHouse. يتجاوز استعلام فحص المخطط عند تعيينه. |
| UseSchemaCache | `bool` | `false` | يخزّن مخطط الجدول الكامل لكل زوج من (database, table) طوال مدة حياة العميل. |

تتوفّر أيضًا جميع خصائص `QueryOptions` في `InsertOptions`.

**مثال:**

```csharp theme={null}
var insertOptions = new InsertOptions
{
    BatchSize = 50_000,
    MaxDegreeOfParallelism = 4,
    QueryId = "bulk-import-001"
};

long rowsInserted = await client.InsertBinaryAsync(
    "my_table",
    columns,
    rows,
    insertOptions
);
```

<h4 id="skip-schema-query">
  تخطي استعلام فحص المخطط
</h4>

بشكل افتراضي، يرسل `InsertBinaryAsync` استعلام `SELECT ... WHERE 1=0` قبل كل عملية إدراج لاكتشاف أنواع الأعمدة. في السيناريوهات ذات معدل النقل المرتفع، يمكنك التخلص من هذا العبء الإضافي باستخدام خيارين:

**الخيار 1: تحديد أنواع الأعمدة صراحةً**

عندما تكون على دراية بمخطط الجدول وقت التجميع، مرّره مباشرةً عبر `ColumnTypes`. عندها لن يُرسل أي استعلام للمخطط على الإطلاق:

```csharp theme={null}
var options = new InsertOptions
{
    ColumnTypes = new Dictionary<string, string>
    {
        ["id"] = "UInt64",
        ["name"] = "Nullable(String)",
        ["score"] = "Float32",
    },
};

await client.InsertBinaryAsync("my_table", ["id", "name", "score"], rows, options);
```

**الخيار 2: تخزين المخطط مؤقتًا**

عند الإدراج في الجدول نفسه بشكل متكرر، اضبط `UseSchemaCache = true` للاستعلام عن المخطط مرة واحدة ثم إعادة استخدامه في عمليات الإدراج اللاحقة ضمن مثيل `ClickHouseClient` نفسه:

```csharp theme={null}
var options = new InsertOptions { UseSchemaCache = true };

// First call fetches schema from the server
await client.InsertBinaryAsync("my_table", columns, batch1, options);

// Second call reuses cached schema — no extra round-trip
await client.InsertBinaryAsync("my_table", columns, batch2, options);
```

<Note>
  * يحظى `ColumnTypes` بالأولوية على `UseSchemaCache`. وإذا تم تعيينهما معًا، فستُستخدم الأنواع المحددة صراحةً.
  * لا تكتشف ذاكرة التخزين المؤقت للمخطط تغييرات `ALTER TABLE`. إذا عدّلت مخطط الجدول، فأنشئ `ClickHouseClient` جديدًا أو تجنّب استخدام `UseSchemaCache` لهذا الجدول.
  * يقتصر نطاق ذاكرة التخزين المؤقت على instance الخاصة بـ `ClickHouseClient`، ويُعرَّف مفتاحها بواسطة (database, table). وتشترك المجموعات الفرعية المختلفة من الأعمدة في الجدول نفسه في مخطط واحد مخزَّن مؤقتًا.
</Note>

<h2 id="clickhouse-client">
  ClickHouseClient
</h2>

تُعد `ClickHouseClient` واجهة برمجة التطبيقات الموصى بها للتفاعل مع ClickHouse. وهي آمنة للاستخدام من عدة خيوط، ومصممة للاستخدام كـ singleton، وتدير داخليًا تجمّع اتصالات HTTP.

<h3 id="creating-a-client">
  إنشاء عميل
</h3>

أنشئ `ClickHouseClient` باستخدام سلسلة اتصال أو كائن `ClickHouseClientSettings`. راجع قسم [التهيئة](#configuration) للاطّلاع على الخيارات المتاحة.

تتوفّر تفاصيل خدمة ClickHouse Cloud الخاصة بك في وحدة تحكم ClickHouse Cloud.

حدّد خدمة وانقر على **Connect**:

<Image img="https://mintcdn.com/private-7c7dfe99-parallel-read-in-order-multi-part/GTkpPcjoRQ_okrH3/images/_snippets/cloud-connect-button.webp?fit=max&auto=format&n=GTkpPcjoRQ_okrH3&q=85&s=d059c1bbcc7317ff8df85b20189e65f4" size="md" alt="زر الاتصال بخدمة ClickHouse Cloud" border width="998" height="932" data-path="images/_snippets/cloud-connect-button.webp" />

اختر **C#**. ستُعرض تفاصيل الاتصال أدناه.

<Image img="https://mintcdn.com/private-7c7dfe99-parallel-read-in-order-multi-part/GTkpPcjoRQ_okrH3/images/_snippets/connection-details-csharp.webp?fit=max&auto=format&n=GTkpPcjoRQ_okrH3&q=85&s=487b14816a8a8711d46ae022d82d74ef" size="md" alt="تفاصيل اتصال ClickHouse Cloud لـ C#" border width="851" height="805" data-path="images/_snippets/connection-details-csharp.webp" />

إذا كنت تستخدم ClickHouse مُدارًا ذاتيًا، فسيُحدِّد مسؤول ClickHouse لديك تفاصيل الاتصال.

باستخدام سلسلة اتصال:

```csharp theme={null}
using ClickHouse.Driver;

using var client = new ClickHouseClient("Host=localhost;Username=default;Password=secret");
```

أو باستخدام `ClickHouseClientSettings`:

```csharp theme={null}
using ClickHouse.Driver;

var settings = new ClickHouseClientSettings
{
    Host = "localhost",
    Username = "default",
    Password = "secret"
};
using var client = new ClickHouseClient(settings);
```

في حالات استخدام حقن التبعية، استخدم `IHttpClientFactory`:

```csharp theme={null}
// In your DI configuration. No AutomaticDecompression needed — the driver decodes
// compressed responses itself, and a mask here would widen its Accept-Encoding.
services.AddHttpClient("ClickHouse", client =>
{
    client.Timeout = TimeSpan.FromMinutes(5);
});

// Create client with factory
var factory = serviceProvider.GetRequiredService<IHttpClientFactory>();
var client = new ClickHouseClient("Host=localhost", factory, "ClickHouse");
```

<Note>
  صُمِّم `ClickHouseClient` ليكون طويل الأمد ويُستخدم بشكل مشترك عبر تطبيقك. أنشِئه مرة واحدة فقط (عادةً بصفته singleton) وأعِد استخدامه في جميع عمليات قاعدة البيانات. يتولى العميل إدارة تجميع اتصالات HTTP داخليًا.
</Note>

***

<h3 id="executing-queries">
  تنفيذ الاستعلامات
</h3>

استخدم `ExecuteNonQueryAsync` مع التعليمات التي لا تُرجع نتائج:

```csharp theme={null}
// Create a table
await client.ExecuteNonQueryAsync(
    "CREATE TABLE IF NOT EXISTS default.my_table (id Int64, name String) ENGINE = Memory"
);

// Drop a table
await client.ExecuteNonQueryAsync("DROP TABLE IF EXISTS default.my_table");
```

استخدم `ExecuteScalarAsync` لاسترجاع قيمة واحدة:

```csharp theme={null}
var count = await client.ExecuteScalarAsync("SELECT count() FROM default.my_table");
Console.WriteLine($"Row count: {count}");

var version = await client.ExecuteScalarAsync("SELECT version()");
Console.WriteLine($"Server version: {version}");
```

***

<h3 id="inserting-data">
  إدخال البيانات
</h3>

<h4 id="parameterized-inserts">
  عمليات الإدراج باستخدام المعلمات
</h4>

أدرِج البيانات باستخدام استعلامات ذات معلمات عبر `ExecuteNonQueryAsync`. يجب تحديد أنواع المعلمات في SQL باستخدام الصياغة `{name:Type}`:

```csharp theme={null}
using ClickHouse.Driver;
using ClickHouse.Driver.ADO.Parameters;

var parameters = new ClickHouseParameterCollection();
parameters.AddParameter("id", 1L);
parameters.AddParameter("name", "Alice");

await client.ExecuteNonQueryAsync(
    "INSERT INTO default.my_table (id, name) VALUES ({id:Int64}, {name:String})",
    parameters
);
```

***

<h4 id="bulk-insert">
  عمليات الإدراج المجمّعة
</h4>

استخدم `InsertBinaryAsync` لإدراج أعداد كبيرة من الصفوف بكفاءة. فهو يمرّر البيانات بتنسيق الصفوف الثنائي الأصلي في ClickHouse، ويدعم تحميل الدُفعات بالتوازي، ويتجنب أخطاء "URL طويل جدًا" التي قد تحدث مع الاستعلامات ذات المعلمات.

```csharp theme={null}
// Prepare data as IEnumerable<object[]>
var rows = Enumerable.Range(0, 1_000_000)
    .Select(i => new object[] { (long)i, $"value{i}" });

var columns = new[] { "id", "name" };

// Basic insert
long rowsInserted = await client.InsertBinaryAsync("default.my_table", columns, rows);
Console.WriteLine($"Rows inserted: {rowsInserted}");
```

لمجموعات البيانات الكبيرة، اضبط التجميع على دفعات ومستوى التوازي باستخدام `InsertOptions`:

```csharp theme={null}
var options = new InsertOptions
{
    BatchSize = 100_000,           // Rows per batch (default: 100,000)
    MaxDegreeOfParallelism = 4     // Parallel batch uploads (default: 1)
};
```

<Note>
  * يجلب العميل تلقائيًا بنية الجدول عبر `SELECT * FROM <table> WHERE 1=0` قبل الإدراج. يجب أن تتوافق القيم المُمرَّرة مع أنواع الأعمدة المستهدفة. لتجاوز هذا الاستعلام، استخدم [`InsertOptions.ColumnTypes` أو `InsertOptions.UseSchemaCache`](#skip-schema-query).
  * عندما تكون قيمة `MaxDegreeOfParallelism > 1`، تُحمَّل الدفعات بالتوازي. لا تتوافق الجلسات مع الإدراج المتوازي؛ لذا عطّل الجلسات أو عيّن `MaxDegreeOfParallelism = 1`.
  * استخدم `RowBinaryFormat.RowBinaryWithDefaults` في `InsertOptions.Format` إذا كنت تريد أن يطبّق الخادم قيم DEFAULT على الأعمدة غير المُمرَّرة.
</Note>

<h4 id="poco-insert">
  عمليات إدراج POCO
</h4>

بدلاً من إنشاء مصفوفات `object[]`، يمكنك إدراج كائنات POCO محددة النوع مباشرةً. سجّل النوع مرة واحدة، ثم مرّر `IEnumerable<T>`:

```csharp theme={null}
// Define a POCO matching your table columns
public class SensorReading
{
    public ulong Id { get; set; }
    public string SensorName { get; set; }
    public double Value { get; set; }
    public DateTime Timestamp { get; set; }
}

// Register the type (once per client lifetime)
client.RegisterBinaryInsertType<SensorReading>();

// Insert directly — column names are derived from property names
var readings = Enumerable.Range(0, 100_000)
    .Select(i => new SensorReading
    {
        Id = (ulong)i,
        SensorName = $"sensor_{i % 10}",
        Value = Random.Shared.NextDouble() * 100,
        Timestamp = DateTime.UtcNow,
    });

long rowsInserted = await client.InsertBinaryAsync("sensors", readings);
```

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

```csharp theme={null}
public class Event
{
    [ClickHouseColumn(Name = "event_id")]     // Map to a differently-named column
    public ulong Id { get; set; }

    [ClickHouseColumn(Type = "LowCardinality(String)")]  // Explicit ClickHouse type
    public string Category { get; set; }

    public string Payload { get; set; }

    [ClickHouseNotMapped]                     // Exclude from insert
    public string InternalTag { get; set; }
}
```

| السمة | الغرض |
| - | - |
| `[ClickHouseColumn(Name = "...")]` | تجاوز اسم العمود الهدف |
| `[ClickHouseColumn(Type = "...")]` | التصريح بنوع ClickHouse صراحةً |
| `[ClickHouseNotMapped]` | استبعاد الخاصية من عملية الإدراج |

عندما تحدد **جميع** الخصائص المعينة `Type` صريحًا، يُتخطّى `استعلام فحص المخطط` بالكامل. وعندما تكون بعض الخصائص فقط ذات أنواع صريحة، يعود `برنامج التشغيل` إلى `فحص المخطط` لمجموعة الأعمدة الكاملة.

يدعم `InsertBinaryAsync<T>` خيارات `InsertOptions` نفسها (التجميع على دفعات، التوازي، التخزين المؤقت للمخطط) مثل التحميل الزائد `object[]`.

<Note>
  بخلاف التحميل الزائد `object[]`، لا يقبل `InsertBinaryAsync<T>` قائمة أعمدة صريحة. تُحدَّد الأعمدة بواسطة الخصائص المعينة للنوع المسجل. للتحكم في الأعمدة التي تُدرج، استخدم `[ClickHouseNotMapped]` لاستبعاد الخصائص أو `[ClickHouseColumn(Name = "...")]` لإعادة تسميتها.

  إذا تم تعيين `ColumnTypes` في `InsertOptions`، فستتجاوز سمات POCO.
</Note>

<h4 id="poco-insert-schema-evolution">
  تطور المخطط
</h4>

تعمل عمليات الإدراج الخاصة بـ POCO بسلاسة عند إضافة أعمدة إلى الجدول المستهدف بعد تسجيل النوع. ونظرًا إلى أن برنامج التشغيل لا يُدرج سوى الأعمدة المرتبطة بـ POCO، فإن أي أعمدة جديدة تحتوي على `DEFAULT` (أو تعبيرات افتراضية أخرى) يملؤها الخادم تلقائيًا. ولا حاجة إلى أي تغييرات في الشيفرة أو إلى إعادة التسجيل.

<h4 id="insert-query-placement">
  موضع استعلام الإدراج
</h4>

يكتب الإدراج الثنائي عبارة `INSERT INTO ... FORMAT ...` الخاصة به في السطر الأول من نص الطلب، قبل الصفوف. ويُضغط نص الطلب افتراضيًا، لذا فإن آليات التوجيه والتسجيل التي تفحص عنوان URL فقط لا ترى هذه العبارة. اضبط `InsertOptions.QueryPlacement` على `InsertQueryPlacement.Url` لإرسال العبارة بدلًا من ذلك في معامل URL باسم `query`، بحيث يقتصر نص الطلب على الصفوف وحدها:

```csharp theme={null}
var options = new InsertOptions { QueryPlacement = InsertQueryPlacement.Url };
await client.InsertBinaryAsync("events", columns, rows, options);
```

استخدمه عندما يقوم proxy أو load balancer أو gateway بالتوجيه أو الفحص بناءً على الـ parameter `query`، أو عندما تريد ظهور الـ statement في سجلات الوصول وأدوات الـ observability. وهو خيار اختياري لأن الـ statement يُحتسب عندئذٍ ضمن طول الـ URL. والحد الفعلي هو الأدنى بين ما يفرضه الـ runtime الخاص بـ .NET والوسيط والـ server. في .NET 6 وحتى .NET 9، يحصر `System.Uri` عنوان URI الكامل المُرمَّز للطلب في 65,519 محرفًا؛ ويطلق الـ driver استثناء `InvalidOperationException` يعيد توجيهك إلى `InsertQueryPlacement.Body` عند تجاوز هذا الحد. أما `http_max_uri_size` في ClickHouse فقيمته الافتراضية 1 ميبي بايت، وقد يفرض الوسيط حدًا أدنى منها. في وضع الـ body، لا يخضع الـ statement ولا الـ rows لأي حد من هذا القبيل يتعلق بطول الـ URL؛ لكن قد تظهر خيارات الطلب الأخرى في الـ URL.

هذا الإعداد مستقل عن `Compressor`: إذ يُرمَّز الـ body بالطريقة نفسها في كلا الوضعين.

***

<h3 id="reading-data">
  قراءة البيانات
</h3>

استخدم `ExecuteReaderAsync` لتنفيذ استعلامات SELECT. يوفّر `ClickHouseDataReader` المُعاد وصولًا مُحدَّد النوع إلى أعمدة النتائج عبر طرق مثل `GetInt64()` و`GetString()` و`GetFieldValue<T>()`.

استدعِ `Read()` للانتقال إلى الصف التالي. وتُرجع `false` عند عدم وجود المزيد من الصفوف. ويمكنك الوصول إلى الأعمدة حسب الفهرس (ابتداءً من 0) أو حسب اسم العمود.

```csharp theme={null}
using ClickHouse.Driver.ADO.Parameters;

var parameters = new ClickHouseParameterCollection();
parameters.AddParameter("max_id", 100L);

var reader = await client.ExecuteReaderAsync(
    "SELECT * FROM default.my_table WHERE id < {max_id:Int64}",
    parameters
);

while (reader.Read())
{
    Console.WriteLine($"Id: {reader.GetInt64(0)}, Name: {reader.GetString(1)}");
}
```

<h4 id="poco-read">
  القراءة باستخدام POCO
</h4>

بدلًا من قراءة الأعمدة حسب الفهرس أو الاسم، يمكنك تمرير نتائج الاستعلام مباشرةً إلى فئاتك الخاصة. سجّل النوع مرة واحدة في العميل، ثم استخدم `QueryAsync<T>`:

```csharp theme={null}
// Define a POCO matching your result columns
public class SensorReading
{
    public ulong Id { get; set; }
    public DateTime Timestamp { get; set; }

    [ClickHouseColumn(Name = "sensor_name")]
    public string SensorName { get; set; }
    public double Value { get; set; }

}

// Register the type (once per client lifetime)
client.RegisterPocoType<SensorReading>();

// Stream results as typed objects
await foreach (var reading in client.QueryAsync<SensorReading>(
    "SELECT Id, sensor_name, Value, Timestamp FROM sensors"))
{
    Console.WriteLine($"{reading.SensorName}: {reading.Value}");
}
```

<h5 id="poco-read-registration">
  التسجيل
</h5>

يُعِدّ `RegisterPocoType<T>()` كلاً من عمليات ربط الإدراج والقراءة، ويتحقق من كليهما مسبقًا. أما `RegisterBinaryInsertType<T>()` فلم يتغير، ويظل مخصصًا للإدراج فقط حفاظًا على التوافق مع الإصدارات السابقة.

يجب أن يتضمن النوع المسجَّل ما يلي:

* مُنشئًا عامًا بدون معاملات.
* خاصية عامة واحدة على الأقل لها مُعدِّل `set` عام وليس `init`. الخصائص `required` مدعومة.

<h5 id="poco-read-column-matching">
  مطابقة الأعمدة
</h5>

مطابقة الأعمدة حسّاسة لحالة الأحرف. تترك أعمدة النتائج المفقودة الخصائص على قيمتها الافتراضية، بينما يتم تجاهل أعمدة النتائج الإضافية.

لا يقوم driver بتوسيع القيم أو تضييقها. وباستثناء التمثيلات البديلة المذكورة أدناه،
يجب أن يكون نوع إطار العمل الخاص بالعمود قابلاً للإسناد إلى نوع الخاصية، ويؤدي عدم التطابق إلى ظهور
`InvalidOperationException`. ولذلك فإن خاصية من النوع `object` تقبل أي عمود.

<h5 id="poco-read-types">
  أنواع الخصائص المدعومة
</h5>

يقرأ `QueryAsync<T>` كلاً من هذه الأعمدة مباشرةً إلى خاصية مطابقة:

| عمود ClickHouse | نوع (أنواع) الخاصية |
| - | - |
| `Int8`/`Int16`/`Int32`/`Int64` | `sbyte`/`short`/`int`/`long` |
| `UInt8`/`UInt16`/`UInt32`/`UInt64` | `byte`/`ushort`/`uint`/`ulong` |
| `Int128`/`UInt128` | `BigInteger`، أو النوع الأصلي `System.Int128`/`System.UInt128` في .NET 8 وما بعده |
| `Int256`/`UInt256` | `BigInteger` |
| `Float32`/`Float64`/`BFloat16` | `float`/`double`/`float` |
| `Bool` | `bool` |
| `Decimal` | `decimal` أو `ClickHouseDecimal` |
| `Date`/`Date32`/`DateTime`/`DateTime64` | `DateTime` أو `DateTimeOffset` أو `DateOnly` |
| `Time`/`Time64` | `TimeSpan` |
| `UUID` | `Guid` |
| `IPv4`/`IPv6` | `IPAddress` |
| `Enum8`/`Enum16` | `string` (التسمية) أو `int` (الترتيب على السلك) |
| `String`/`FixedString` | `string` أو `byte[]` |

يقبل كل صف أيضاً الصيغة القابلة لقيمة NULL من نوع خاصيته (`long?` و`DateOnly?` وما إلى ذلك)،
سواء أكان العمود `Nullable(...)` أم لا. أما الخاصية من نوع قيمي غير قابل لقيمة NULL على عمود
`Nullable(T)` فتُقبل عند التسجيل، لكنها تُطلق استثناءً عند وصول قيمة NULL.

تُطابَق الأغلفة مثل `LowCardinality(T)` و`SimpleAggregateFunction(f, T)` و`Object(T)` تماماً كما لو كانت `T`.

الأعمدة المركّبة مدعومة أيضاً، وتأخذ نوع إطار العمل المذكور في
[مرجع أنواع القراءة](#clickhouse-native-type-map-reading): `Array(T)` إلى `T[]`، و`Tuple(...)`
إلى `System.Tuple<...>`، و`Nested(...)` إلى `Tuple<...>[]`، و`JSON` إلى `JsonObject` (أو `string`
في وضع [`JsonReadMode=String`](#type-map-reading-json))، و`Variant`/`Dynamic` إلى `object`.

ويمثّل العمود من نوع `Map(K, V)` حالة خاصة: فالخاصية من نوع `List<KeyValuePair<K, V>>` أو `KeyValuePair<K, V>[]`
تُقرأ عبر المسار الخالي من التغليف (box-free)، وتحافظ على الترتيب على السلك وعلى أي مفاتيح مكرّرة، في كلا
وضعي [`MapReadMode`](#type-map-reading-map). أما خاصية `Dictionary<K, V>` فتعمل في الوضع الافتراضي
فقط. ويجب أن يتطابق نوعا المفتاح والقيمة تماماً، لذا يتطلب
`Map(String, Nullable(Int32))` النوع `KeyValuePair<string, int?>`.

وحين يتيح العمود أكثر من نوع خاصية (عمود `DateTime` بصيغة `DateTime`
أو `DateTimeOffset` أو `DateOnly`، وعمود `String` بصيغة `string` أو `byte[]`)، فإن نوع الخاصية المُعلَن هو ما يحدّد التمثيل. وهذه التمثيلات البديلة تخصّ
مسار POCO، لذا يوفّرها `QueryAsync<T>` بينما لا يوفّرها `MapTo<T>`.

<h5 id="poco-read-mapto">
  تحويل صف واحد إلى كائن
</h5>

عند التكرار على القارئ يدويًا، استخدم `ClickHouseDataReader.MapTo<T>()` لتحويل الصف الحالي إلى POCO مسجَّل دون تحريك القارئ إلى الصف التالي:

```csharp theme={null}
var reader = await client.ExecuteReaderAsync("SELECT Id, SensorName, Value, Timestamp FROM sensors");

while (reader.Read())
{
    SensorReading reading = reader.MapTo<SensorReading>();
    Console.WriteLine($"{reading.SensorName}: {reading.Value}");
}
```

استخدم `MapTo<T>` عندما تحتاج إلى إدارة حلقة القارئ بنفسك — على سبيل المثال لمزج الوصول المباشر إلى الأعمدة
مع تحويل الصفوف إلى كائنات POCO. فهو يقرأ الصف عبر القيم المُعلَّبة الخاصة بالقارئ، لذا لا يوفر
أنواع الخصائص البديلة المذكورة أعلاه، كما أنه يستهلك ذاكرة أكثر من `QueryAsync<T>`. ويُفضَّل استخدام
`QueryAsync<T>` إذا كنت تحتاج إلى الصفوف فقط؛ راجع
[اختيار مسار التحويل إلى كائنات](#perf-read-path) للاطلاع على الأرقام.

<h5 id="poco-read-converters">
  محوّلات قيم القراءة
</h5>

يُطبَّق [محوّل قيم القراءة](#read-value-conversion) المُعرَّف على مستوى العميل أو لكل استعلام على كلا المسارين،
ولا يعطّل القراءة الخالية من التغليف. يحوّل الـ driver كل عمود باستخدام التحميل الزائد المطابق للطريقة التي
قرأ بها العمود: `ConvertValue<T>` المُنمَّط للعمود الخالي من التغليف،
و`ConvertValue` المُغلَّف للعمود المركّب. احرص على تنفيذ التحميلين الزائدين
بشكل متسق، وإلا فسيعطي العمود نفسه نتائج مختلفة باختلاف المسار.

<h5 id="poco-read-diagnostics">
  تشخيصات التسجيل
</h5>

عند تهيئة `LoggerFactory`، يُصدر كلٌّ من `RegisterPocoType<T>()` و`RegisterBinaryInsertType<T>()` سجلًا على مستوى `Debug` (ضمن الفئة `ClickHouse.Driver.Client`) يبيّن الخصائص التي طابقت الأعمدة، وتلك التي جرى تخطيها وسبب ذلك. راجع [التسجيل والتشخيصات](#logging-and-diagnostics).

***

<h3 id="sql-parameters">
  معلمات SQL
</h3>

في ClickHouse، الصيغة القياسية لمعلمات الاستعلام في استعلامات SQL هي `{parameter_name:DataType}`.

**أمثلة:**

```sql theme={null}
SELECT {value:Array(UInt16)} as a
```

```sql theme={null}
SELECT * FROM table WHERE val = {tuple_in_tuple:Tuple(UInt8, Tuple(String, UInt8))}
```

```sql theme={null}
INSERT INTO table VALUES ({val1:Int32}, {val2:Array(UInt8)})
```

<Note>
  تُمرَّر معلمات SQL ‏'bind' كمعلمات استعلام في HTTP URI، لذا فإن استخدام عدد كبير جدًا منها قد يؤدي إلى ظهور استثناء "URL طويل جدًا". استخدم `InsertBinaryAsync` لإدراج كميات كبيرة من البيانات لتجنّب هذا القيد.
</Note>

<h4 id="at-style-placeholders">
  العناصر النائبة `@name` بأسلوب ADO
</h4>

يقبل الـ driver أيضًا العناصر النائبة `@name` التي تُصدرها أدوات ORM مثل Dapper. وهي مجرد تسهيل على
جهة العميل: فقبل إرسال الطلب، يُعاد كتابة كل عنصر منها إلى الصيغة
`{name:ResolvedType}`، بحيث لا يرى الـ server علامة `@` مطلقًا. راجع
[تحديد النوع](#parameter-type-mapping) لمعرفة كيفية اختيار النوع. ويُفضّل استخدام الصيغة الصريحة
`{name:Type}` كلما أمكن ذلك.

أما `@name` الذي لا يقابله أي parameter فيُترك كما هو ليرفضه الـ server. والمطابقة
حساسة لحالة الأحرف، لذا فإن `@ID` لا يرتبط بـ parameter باسم `id`.

<Note>
  لتعطيل إعادة الكتابة، فعّل مفتاح AppContext المسمى `ClickHouse.Driver.DisableReplacingParameters`
  قبل أول استخدام للـ driver. عندئذٍ تتوقف إعادة كتابة النص فقط، أما الـ parameters فتُرسل كما هي، ومن ثمّ تظل
  الاستعلامات المكتوبة بصيغة `{name:Type}` الأصلية تعمل.
</Note>

<h4 id="identifier-parameters">
  معلمات Identifier
</h4>

يتيح لك نوع المعلمة `Identifier` ربط اسم قاعدة بيانات أو جدول أو عمود بأمان بدلًا من استخدام قيمة حرفية نصية بين علامتَي اقتباس. استخدمه بصيغة `{name:Identifier}` في SQL، أو عبر تعيين `ClickHouseDbParameter.ClickHouseType = "Identifier"`:

```csharp theme={null}
var parameters = new ClickHouseParameterCollection();
parameters.AddParameter("name", "my_database");

await client.ExecuteNonQueryAsync("CREATE DATABASE {name:Identifier}", parameters);
```

```csharp theme={null}
var parameters = new ClickHouseParameterCollection();
parameters.AddParameter("col", "user_id");

var reader = await client.ExecuteReaderAsync("SELECT {col:Identifier} FROM t", parameters);
```

تُرسَل القيمة كما هي حرفيًا، ويستبدلها الخادم كمُعرّف SQL خام، مع تطبيق الإحاطة بعلامات `backtick` وإفلات الأحرف الخاصة وفق آليته الخاصة. ويمكن نقل المعرّفات التي تحتوي على أحرف خاصة (بما في ذلك علامات `backtick`) ذهابًا وإيابًا بأمان.

***

<h3 id="query-id">
  معرّف الاستعلام
</h3>

يُخصَّص لكل استعلام معرّف فريد `query_id` يمكن استخدامه لجلب البيانات من جدول `system.query_log` أو لإلغاء الاستعلامات طويلة التشغيل. يمكنك تحديد معرّف استعلام مخصّص عبر `QueryOptions`:

```csharp theme={null}
var options = new QueryOptions
{
    QueryId = $"report-{Guid.NewGuid()}"
};

var reader = await client.ExecuteReaderAsync(
    "SELECT * FROM large_table",
    parameters: null,
    options: options
);
```

<Tip>
  إذا كنت تحدد `QueryId` مخصصًا، فتأكد من أنه فريد في كل استدعاء. ويُعد GUID عشوائيًا خيارًا جيدًا.
</Tip>

***

<h3 id="parameter-type-mapping">
  تعيين نوع المعلمة المخصّص
</h3>

عند استخدام المعلمات بأسلوب `@` (مثل `WHERE id = @id`)، يستنتج برنامج التشغيل تلقائيًا نوع ClickHouse من نوع القيمة في ‎.NET. على سبيل المثال، يُعيَّن `int` إلى `Int32`.

<Warning>
  **سلوك معلمات DateTime المستنتجة**

  بالنسبة إلى المعلمات بأسلوب `@` التي لا تحتوي على تلميح `{name:Type}` في SQL ولم يُعيَّن لها `ClickHouseType`، تُستنتج القيم التي تمثل نقطة زمنية على أنها `DateTime('UTC')` بدلًا من `DateTime` مجردة. ويُرسَل `DateTime` الذي تكون فيه قيمة `Kind` هي `Utc` أو `Local`، وجميع قيم `DateTimeOffset`، على هيئة `DateTime('UTC')`، مع الحفاظ على النقطة الزمنية عبر أي `server timezone`.

  تكون للتلميحات الصريحة (`{name:DateTime}`) أولوية أعلى من الاستدلال، وهي الطريقة الموصى بها لبناء الاستعلامات.
</Warning>

لتجاوز هذه الإعدادات الافتراضية، عيّن `ParameterTypeResolver` في `ClickHouseClientSettings`. ويكون ذلك مفيدًا عندما تريد استخدام `DateTime64(3)` لجميع معلمات `DateTime` بدقة الملّي ثانية، أو استخدام قيمة `scale` محددة لجميع قيم Decimal، من دون تعيين `ClickHouseType` لكل معلمة على حدة.

**استخدام `DictionaryParameterTypeResolver` لتعيينات الأنواع البسيطة:**

```csharp theme={null}
using ClickHouse.Driver.ADO.Parameters;

var settings = new ClickHouseClientSettings("Host=localhost")
{
    ParameterTypeResolver = new DictionaryParameterTypeResolver(new Dictionary<Type, string>
    {
        [typeof(DateTime)] = "DateTime64(3)",
        [typeof(decimal)] = "Decimal64(4)",
    }),
};
using var client = new ClickHouseClient(settings);

var parameters = new ClickHouseParameterCollection();
parameters.AddParameter("dt", DateTime.UtcNow);     // Mapped to DateTime64(3)
parameters.AddParameter("amount", 99.1234m);         // Mapped to Decimal64(4)

await client.ExecuteReaderAsync("SELECT @dt, @amount", parameters);
```

**`IParameterTypeResolver` مخصّص للحالات المتقدمة:**

للاستدلال المعتمد على القيمة أو المستند إلى الاسم، نفِّذ الواجهة `IParameterTypeResolver` مباشرةً. أرجِع `null` للانتقال إلى الاستدلال الافتراضي:

```csharp theme={null}
public class SmartDecimalResolver : IParameterTypeResolver
{
    public string ResolveType(Type clrType, object value, string parameterName)
    {
        if (clrType != typeof(decimal))
            return null; // Fall through to default

        var scale = (decimal.GetBits((decimal)value)[3] >> 16) & 0x7F;
        return scale <= 4 ? $"Decimal64({scale})" : $"Decimal128({scale})";
    }
}
```

يمكنك أيضًا تعيين محلِّل لاستعلام واحد عبر `QueryOptions.ParameterTypeResolver`. وعند تعيينه، تكون له أولوية أعلى من المحلِّل على مستوى العميل.

**ترتيب أولوية تحديد النوع:**

المحلِّل ليس سوى خطوة ضمن سلسلة ترتيب الأولوية. من الأعلى إلى الأدنى أولوية:

1. تعيين `ClickHouseType` صراحةً على المعلَمة
2. تلميح نوع SQL من الصيغة `{name:Type}` في الاستعلام
3. `IParameterTypeResolver` (من `QueryOptions.ParameterTypeResolver`، مع الرجوع إلى `ClickHouseClientSettings.ParameterTypeResolver`)
4. استنتاج النوع المضمَّن (`TypeConverter.ToClickHouseType`)

يعمل المحلِّل أيضًا مع مسار ADO.NET `ClickHouseConnection` — إذ ترث الاتصالات المُنشأة من العميل هذه الإعدادات.

***

<h3 id="parameter-value-formatting">
  تنسيق مخصص لقيم المعلمات
</h3>

`IParameterFormatter` هو خطاف يحدّد كيفية تسلسل قيم المعلمات. استخدمه عندما لا يتوافق التنسيق المضمّن (مثل دقة DateTime، والإعدادات المحلية لـ Decimal، وإفلات السلاسل النصية، وتمثيل الأرقام) مع ما يتوقعه المخطط أو الأدوات اللاحقة.

عيّن `ParameterFormatter` في `ClickHouseClientSettings` لتثبيت منسّق لجميع الاستعلامات المعلَّمة بمعلمات. يتلقى المنسّق القيمة، واسم نوع ClickHouse الذي جرى تحديده، واسم المعلمة، ويُرجع التمثيل النصي الذي يُرسَل إلى الخادم. أعد `null` للرجوع إلى المنسّق الافتراضي.

**استخدام `DictionaryParameterFormatter` للتنسيق البسيط لكل نوع CLR على حدة:**

```csharp theme={null}
using ClickHouse.Driver.ADO.Parameters;

var settings = new ClickHouseClientSettings("Host=localhost")
{
    ParameterFormatter = new DictionaryParameterFormatter(new Dictionary<Type, Func<object, string>>
    {
        [typeof(DateTime)] = v => ((DateTime)v).ToString("yyyy-MM-ddTHH:mm:ss.ffffff",
            System.Globalization.CultureInfo.InvariantCulture),
        [typeof(decimal)] = v => ((decimal)v).ToString("F4",
            System.Globalization.CultureInfo.InvariantCulture),
    }),
};
using var client = new ClickHouseClient(settings);
```

**`IParameterFormatter` مخصّص للحالات المتقدمة:**

```csharp theme={null}
public class FixedDecimalFormatter : IParameterFormatter
{
    public string Format(object value, string typeName, string parameterName)
    {
        if (value is decimal d)
            return d.ToString("F4", System.Globalization.CultureInfo.InvariantCulture);
        return null; // Fall through for anything else
    }
}
```

يمكنك أيضًا تعيين منسّق لكل استعلام عبر `QueryOptions.ParameterFormatter`. وعند تعيينه، تكون له الأولوية على المنسّق على مستوى العميل.

**القيم المركّبة:**

يُطبَّق المنسّق على معلمات المجموعات ذات المستوى الأعلى، وكذلك على كل عنصر داخل القيم المركّبة (`Array`, `Tuple`, `Map`, `Nullable`, `LowCardinality`, `Variant`). على سبيل المثال، تؤدي مطابقة `typeof(int)` إلى تنسيق كل عنصر `Int32` داخل `Array(Int32)` على حدة.

**الإحاطة بعلامات الاقتباس المفردة في السياقات المركّبة:**

بالنسبة إلى أنواع ClickHouse الشبيهة بالسلاسل النصية (`String`, `FixedString`, `Enum8`, `Enum16`, `IPv4`, `IPv6`, `UUID`) المضمّنة داخل قيمة حرفية مركّبة، يحيط برنامج التشغيل مخرجات المنسّق بعلامات اقتباس مفردة، لكنه لا يجري إفلاتًا لمحتواها. إذا كانت السلسلة التي يعيدها المنسّق تحتوي على علامة اقتباس مفردة أو شرطة مائلة عكسية غير مُفلَتة، فستصبح القيمة الحرفية المركّبة غير صحيحة، وسيرفض الخادم الاستعلام.

تُستخدم معلمات السلاسل النصية ذات المستوى الأعلى (غير المضمّنة داخل قيمة مركّبة) كما هي، من دون إحاطة، لذا لا يلزم الإفلات هنا.

**أولوية المنسّق:**

1. `IParameterFormatter` (من `QueryOptions.ParameterFormatter`، مع الرجوع إلى `ClickHouseClientSettings.ParameterFormatter`). إذا أعاد قيمة غير `null`، فستُستخدم هذه القيمة.
2. التنسيق المضمّن الخاص بالنوع في `HttpParameterFormatter`.

لا يُستشار المنسّق لقيم `null` أو `DBNull`؛ إذ تُسلسَل هذه القيم دائمًا باعتبارها مؤشر ClickHouse للقيم null (`\N`).

***

<h3 id="read-value-conversion">
  تحويل مخصص لقيم القراءة
</h3>

يتيح لك `IReadValueConverter` تحويل القيم التي يعيدها قارئ البيانات بعد إلغاء التسلسل، من دون تغيير نوع CLR الخاص بها. ومن الاستخدامات الشائعة: تعيين `DateTime.Kind = Utc` لعمود `DateTime` لا يحتوي على timezone، أو إزالة المسافات الزائدة من السلاسل النصية أو تطبيعها، أو إجراء معالجة لاحقة على عمود JSON قبل أن يصل إلى شيفرة التطبيق.

عيّن `ReadValueConverter` في `ClickHouseClientSettings` لتطبيق محوّل على جميع عمليات القراءة. ويُستدعى المحوّل مرة واحدة لكل عمود في كل صف عبر كلٍّ من المسار المغلّف (`GetValue`) والمسار العام (`GetFieldValue<T>`). وعند عدم تعيين أي محوّل، لا توجد أي كلفة إضافية — إذ يعيد القارئ القيم مباشرةً.

**استخدام `DictionaryReadValueConverter` لإجراء تحويل بسيط لكل نوع CLR:**

```csharp theme={null}
using ClickHouse.Driver.ADO.Readers;

var converter = new DictionaryReadValueConverter()
    .For<DateTime>(dt => DateTime.SpecifyKind(dt, DateTimeKind.Utc))
    .For<string>(s => s.Trim());

var settings = new ClickHouseClientSettings("Host=localhost")
{
    ReadValueConverter = converter,
};
using var client = new ClickHouseClient(settings);
```

تمرّ القيم التي لم يُسجَّل نوع CLR الخاص بها وقت التشغيل باستخدام `For<T>` كما هي من دون تغيير. ويجري التوجيه وفق نوع CLR المطابق تمامًا، لذا سجّل النوع الفعلي الذي يُنتجه القارئ (على سبيل المثال، `For<JsonObject>` لعمود JSON في `JsonReadMode.Binary`).

**`IReadValueConverter` مخصّص للحالات المتقدمة:**

إذا كنت بحاجة إلى التوجيه استنادًا إلى سلسلة النوع من جانب ClickHouse (على سبيل المثال، للتمييز بين `DateTime` و`DateTime('UTC')` — إذ يظهر كلاهما على أنهما نوع CLR نفسه)، فنفّذ `IReadValueConverter` مباشرةً:

```csharp theme={null}
public class UtcKindForNoTzDateTimeConverter : IReadValueConverter
{
    public object ConvertValue(object value, string columnName, string clickHouseType)
    {
        if (value is DateTime dt && clickHouseType == "DateTime")
            return DateTime.SpecifyKind(dt, DateTimeKind.Utc);
        return value;
    }

    public T ConvertValue<T>(T value, string columnName, string clickHouseType)
    {
        if (typeof(T) == typeof(DateTime) && value is DateTime dt && clickHouseType == "DateTime")
            return (T)(object)DateTime.SpecifyKind(dt, DateTimeKind.Utc);
        return value;
    }
}
```

يجب أن يحافظ المحوّل على نوع CLR وقت التشغيل؛ إذ لا يتم تمرير البيانات الوصفية للأعمدة (`GetFieldType`, `GetSchemaTable`) عبره، ويجب أن تظل متسقة مع ما يتم إرجاعه.

يمكنك أيضًا تعيين محوّل لكل استعلام عبر `QueryOptions.ReadValueConverter`؛ وعند تعيينه، تكون له الأولوية على المحوّل على مستوى العميل.

**حدود الإرسال:**

يُستدعى المحوّل مرة واحدة لكل عمود مع قيمة الخلية كاملة بعد فك التسلسل، وهو **لا** يتعمق تكراريًا داخل الحاويات المركبة. بالنسبة إلى عمود `Array(Int32)`، تكون القيمة المُمرَّرة هي `int[]`؛ وبالنسبة إلى `Tuple(Int32, String)`، تكون `ITuple`.

**أي تحميل زائد يُنفَّذ:**

يجب أن يتوافق التحميلان الزائدان، لأن التحميل الذي يستدعيه الـ driver يعتمد على الطريقة التي قرأ بها المستدعي
العمود:

* `ConvertValue<T>` — الوصولات المُحدَّدة النوع `GetByte`، و`GetSByte`، و`GetInt16`/`32`/`64`،
  و`GetUInt16`/`32`/`64`، و`GetFloat`، و`GetDouble`، و`GetGuid`، و`GetDateTime`، و`GetIPAddress`،
  و`GetBigInteger` و`GetFieldValue<T>`، إضافةً إلى كل عمود خالٍ من التغليف في
  [مسار قراءة POCO](#poco-read-converters).
* `ConvertValue` (المغلّف) — `GetValue`، و`GetValues`، والمُفهرِسات، و`GetChar`، و`GetTuple`، و
  المسارات القسرية في `GetBoolean` و`GetDecimal` و`GetString`.

لا يُشغّل `IsDBNull` أي محوّل على الإطلاق: فهو يقرأ راية القيمة الفارغة مباشرةً، لذا لا يمكن لأي محوّل أن
يغيّر ما إذا كانت القيمة تُعدّ فارغة. وكذلك يتجاوزه `TryGetEnumOrdinal` — راجع
[قراءة الترتيب في enum](#ado-net-reader-enum-ordinal).

يعمل المحوّل مع مسار `ClickHouseConnection` في ADO.NET — وترث الاتصالات المُنشأة من العميل هذه الإعدادات.

***

<h3 id="raw-streaming">
  البث الخام
</h3>

استخدم `ExecuteRawResultAsync` لبث نتائج الاستعلام مباشرةً بتنسيق محدد، متجاوزًا قارئ البيانات. يفيد ذلك عند تصدير البيانات إلى ملفات أو تمريرها إلى أنظمة أخرى:

```csharp theme={null}
using var result = await client.ExecuteRawResultAsync(
    "SELECT * FROM default.my_table LIMIT 100 FORMAT JSONEachRow"
);

await using var stream = await result.ReadAsStreamAsync();
using var reader = new StreamReader(stream);
var json = await reader.ReadToEndAsync();
```

التنسيقات الشائعة: `JSONEachRow`، `CSV`، `TSV`، `Parquet`، `Native`. راجع [توثيق التنسيقات](/ar/reference/formats/index) للتعرّف على جميع الخيارات.

***

<h3 id="per-query-accept-encoding">
  ضغط النقل لكل استعلام
</h3>

افتراضيًا، يتفاوض العميل على `zstd, lz4, gzip, deflate` عندما تكون `Compression=true` (وهو الإعداد الافتراضي في سلسلة الاتصال)، ويتولى فك ترميز الدفق بنفسه تلقائيًا وبشفافية.

بالنسبة إلى عمليات التصدير الخام (مثل Parquet وArrow وNative)، قد ترغب في التفاوض على codec مختلف (مثل `zstd` أو `lz4`) لتحقيق مفاضلة بين استهلاك CPU وعرض النطاق الترددي من دون تغيير الإعداد على مستوى الاتصال بالكامل. يضبط كلٌّ من `QueryOptions.AcceptEncoding` و`ClickHouseCommand.AcceptEncoding` ترويسة HTTP ‏`Accept-Encoding` لطلب واحد، مع استبدال أي قيمة افتراضية كانت معيّنة، ويفرضان `enable_http_compression=1` على `URL` (وهو ما يتطلبه ClickHouse قبل أن يلتزم بـ `Accept-Encoding`).

```csharp theme={null}
using var result = await client.ExecuteRawResultAsync(
    "SELECT * FROM events FORMAT Parquet",
    options: new QueryOptions { AcceptEncoding = "zstd" });

// Decode yourself or write to a file
await using var body = await result.ReadAsStreamAsync();
```

<h4 id="per-query-accept-encoding-httpclient">
  تهيئة HttpClient
</h4>

لا شيء يحتاج إلى تهيئة: فالـ `HttpClient` الذي ينشئه الـ driver يترك `AutomaticDecompression` عند `DecompressionMethods.None` ويتولى الـ driver فك ترميز الاستجابات بنفسه، لذا لا يُحذف `Content-Encoding` من دون علمك، ويصلك الـ body الخام تمامًا كما أرسله الـ server.

<Warning>
  إذا وفّرت `HttpClient` خاصًا بك، فاترك `AutomaticDecompression` معطّلًا أيضًا. فهو ليس إعدادًا يخص جانب الاستجابة فحسب: فعند الإرسال يقوم الـ handler بـ**إضافة كل خوارزمية موجودة في قناعه وغائبة عن `Accept-Encoding` الصادر**. لذا فإن handler يحمل `GZip | Deflate` يحوّل `AcceptEncoding = "lz4"` الصريح إلى `lz4, gzip, deflate`، ويحوّل `"identity"` الصريحة إلى `identity, gzip, deflate` في تنسيق النقل — وبما أن ClickHouse يحسم الـ header وفق أفضلية codec ثابتة خاصة به (متجاهلًا الترتيب وقيم q)، فقد يردّ بـ codec لم تطلبه قط، ثم يفك الـ handler ترميزه ويحذفه فلا تلاحظ حتى أن ذلك قد حدث. أما ترك القناع معطّلًا فيُبقي العرض المقدَّم مطابقًا تمامًا لما اخترته.
</Warning>

<Warning>
  إذا طلب `AcceptEncoding` codec لا يستطيع الـ driver فك ترميزه (`snappy`)، فإن `ExecuteRawResultAsync` وحدها هي الآمنة. أما `ExecuteReaderAsync` و`ExecuteScalarAsync` و`ExecuteNonQueryAsync` فتفشل برمي `NotSupportedException` يذكر اسم الـ codec (وكانت سابقًا تحلل الـ compressed bytes على أنها format النتيجة فتنتج مهملات).
</Warning>

<h4 id="per-query-accept-encoding-errors">
  أجسام رسائل الخطأ
</h4>

عندما يستجيب الخادم بحالة 4xx/5xx وكان قد جرى تعيين `enable_http_compression=1`، فإنه يضغط جسم الخطأ باستخدام الـ codec نفسه الذي كان سيستخدمه في الاستجابة الناجحة. ويفك برنامج التشغيل ترميز هذه الأجسام لكل codec يدعمه (`lz4`, `zstd`, `gzip`, `deflate`, `br`/`brotli`)، بحيث تكون الرسالة الظاهرة في `ClickHouseServerException` قابلة للقراءة. أما ما عدا ذلك (`snappy`, …) فيُرجع رسالة بديلة تذكر اسم الـ codec وتشير إلى `system.query_log` للاطلاع على نص الخطأ الأصلي.

***

<h3 id="response-decompression">
  فك ضغط الاستجابة
</h3>

لا يطلب `Accept-Encoding` من الخادم سوى ضغط الاستجابة — ويبقى فك ترميزها مهمة جهة أخرى. ويتولى driver ذلك بنفسه اعتماداً على `Content-Encoding` الخاص بالاستجابة، ولذلك تعمل جميع واجهات القراءة المعتادة (`ExecuteReaderAsync`، `ExecuteScalarAsync`، `ExecuteNonQueryAsync`، `QueryAsync<T>`، Dapper، EF Core، linq2db) مع الاستجابة المضغوطة دون الحاجة إلى أي تهيئة. وهو يفك ترميز `lz4` و`zstd` و`gzip` و`deflate` و`br`؛ أما `snappy` فغير مدعوم.

افتراضياً يُعلن driver عن **`zstd, lz4, gzip, deflate`**، ويردّ ClickHouse بـ `zstd`. ولاختيار غير ذلك، عيّن `Accept-Encoding` بنفسك — على مستوى client بالكامل:

```csharp theme={null}
using var client = new ClickHouseClient(new ClickHouseClientSettings("Host=localhost")
{
    AcceptEncoding = "br",      // decodable, but not advertised by default
});
```

لكل استعلام، وهو ما تكون له الأسبقية:

```csharp theme={null}
using var reader = await client.ExecuteReaderAsync(
    "SELECT * FROM events",
    options: new QueryOptions { AcceptEncoding = "identity" });   // opt this query out
```

أو في connection string، لمستخدمي ORM الذين لا يتعاملون مطلقًا مع `ClickHouseClientSettings`:

```text theme={null}
Host=localhost;AcceptEncoding=br, gzip
```

كما أنّ ضبطه يفرض `enable_http_compression=1` في الـ URL، وهو ما يشترطه ClickHouse قبل أن يأخذ الـ header بعين الاعتبار أصلًا — بما في ذلك عندما تكون `UseCompression` بقيمة `false`، إذ إنّ تسمية codec صراحةً تُعدّ طلبًا له. وفي حال عدم ضبط أي قيمة، فإنّ `UseCompression=false` لا ترسل أي `Accept-Encoding` إطلاقًا.

يمكن ضبط `Accept-Encoding` في أربعة مواضع، ويُعتمد أوّل موضع منها يسمّي codec:

1. `QueryOptions.AcceptEncoding` (أو `ClickHouseCommand.AcceptEncoding`)
2. `CustomHeaders["Accept-Encoding"]` على مستوى الاستعلام
3. `CustomHeaders["Accept-Encoding"]` على مستوى العميل
4. `ClickHouseClientSettings.AcceptEncoding`، أو الكلمة المفتاحية `AcceptEncoding` في سلسلة الاتصال

وإذا لم يسمِّ أيٌّ منها codec، يرسل الـ driver قائمته الافتراضية. أمّا القيمة التي لا تسمّي أي codec (null، أو فارغة،
أو مسافات بيضاء، أو فواصل فقط) فتُعدّ غير مضبوطة ويُنتقل إلى الموضع التالي. ولتعطيل الضغط، استخدم `identity`.

**الخادم، لا العميل، هو من يختار الـ codec.** يفحص ClickHouse قيمة `Accept-Encoding` بحثًا عن الرموز وفق ترتيب أفضلية ثابت خاص به — `zstd` > `br` > `lz4` > `snappy` > `gzip` > `deflate` — متجاهلًا الترتيب الذي تسردها به وأي قيم q. وبذلك يكون الـ header إعلانًا عن القدرات لا طلبًا مُلزمًا، والسبيل الوحيد للتأثير في الاختيار هو استبعاد بعض الرموز. وتتضمّن القائمة الافتراضية `zstd`، لذا يُجاب على الاستعلام الافتراضي بضغط zstd، بينما تعمل بقية الرموز كخيار احتياطي. ويمكن فكّ ترميز `br` غير أنّه غير مُعلن عنه افتراضيًا.

أمّا المقارنة بين الـ codecs من حيث حجم الـ payload واستهلاك CPU على الخادم وعلى العميل، فتتوقّف على بياناتك ووصلتك وعلى قيمة `http_zlib_compression_level` في الخادم (القيمة الافتراضية المُرفقة: 3) — راجع [ضبط الضغط](#tuning-compression).

* **`http_zlib_compression_level`.** ينطبق هذا الإعداد على كل codec خاص بـ HTTP، وقيمته الافتراضية هي 3. وينبغي ضبط هذه القيمة استنادًا إلى بياناتك وسرعة الوصلة واستهلاك CPU.
* **عميل مقيَّد بـ CPU على وصلة سريعة.** يفكّ الـ driver ترميز جسم الاستجابة على الـ thread المستدعي، لذا عندما لا تكون الشبكة هي عنق الزجاجة، قد تصبح سرعة فكّ الترميز في جهة العميل هي العامل المحدِّد.

اطلب codec مختلفًا على مستوى الاستعلام، أو على مستوى العميل بأكمله، متى انطبقت أي من الحالتين:

```csharp theme={null}
using var reader = await client.ExecuteReaderAsync(
    "SELECT * FROM events",
    options: new QueryOptions { AcceptEncoding = "lz4" });   // decode this one with lz4 instead
```

ولأن القرار يُتخذ بناءً على الاستجابة، فإن الـ body يُفك ضغطه كلما أشار رأس `Content-Encoding` الخاص به إلى ذلك، أيًّا كان ما طُلب: فإن كان الرأس غائبًا أو قيمته `identity` يمرّ المحتوى دون تغيير، وإن كان codec مدعومًا فيُفك ضغطه، وأي شيء آخر يُثير error يذكر اسمه. ولا خطر من فك الضغط مرتين — فإذا كان handler مُقدَّم من الـ caller قد فكّ ضغط الـ body عبر `AutomaticDecompression`، فإنه يزيل أيضًا `Content-Encoding`، وبذلك يرى الـ driver بيانات plaintext فيتركها كما هي.

**النتائج الخام لا تُعلن عن أي codec.** تُسلّمك `ExecuteRawResultAsync` (وكذلك `PostStreamAsync` / `InsertRawStreamAsync` العامتان) الـ body كما هو حرفيًا، لذا ما لم تحدّد codec بنفسك فهي لا تطلب أي codec إطلاقًا — فلا شيء في الـ driver يفك ترميز مثل هذا الـ body، ومن ثَمّ فإن تقديم codec هناك سيحوّل عملية التصدير إلى File مضغوط دون أن تدري. لذا فالقاعدة بسيطة ولا تتأثر بكيفية تهيئة أي `HttpClient`: **الـ body الحرفي يصل تمامًا كما أرسله الـ server، والـ server يرسل plaintext ما لم تطلب codec.** وطلبُ codec (على مستوى الـ client بالكامل أو لكل query) هو الطريقة التي تصدّر بها compressed bytes عن قصد.

ويظل تحديد `AcceptEncoding` صراحةً (على أي من المستويين) ساريًا على الـ requests الخام، كما تفك `ClickHouseRawResult.ReadDecompressedStreamAsync()` ترميز النتيجة متى أردت ذلك؛ أما `ReadAsStreamAsync` و`ReadAsByteArrayAsync` و`ReadAsStringAsync` و`CopyToAsync` فتُعيد دائمًا البايتات تمامًا كما وصلت.

```csharp theme={null}
using var result = await client.ExecuteRawResultAsync(
    "SELECT * FROM events FORMAT JSONEachRow",
    options: new QueryOptions { AcceptEncoding = "lz4" });

Console.WriteLine(result.ContentEncoding); // "lz4"

await using var body = await result.ReadDecompressedStreamAsync();
using var bodyReader = new StreamReader(body);
var json = await bodyReader.ReadToEndAsync();
```

اقرأ الـ stream المُعاد حتى نهايته قبل خروجه من الـ scope، كما في المثال أعلاه. فعندما تكون الاستجابة مضغوطة *فعلاً*، تحصل على decoder مُنشأ باستخدام `leaveOpen`، ومن ثم فإن التخلص منه يُبقي الاستجابة سليمة؛ أما عندما تكون **غير** مضغوطة، فتحصل على stream محتوى HTTP ذاته، وبالتالي فإن التخلص منه يُنهي الـ body. وفي كلتا الحالتين يمتلك `ClickHouseRawResult` الاستجابة — فلا تستدعِ أعضاء القراءة الأخرى فيه بعد التخلص من الـ stream. والتخلص من `ClickHouseRawResult` مطلوب دائماً وكافٍ بذاته: فهو يحرّر الاستجابة وأي decoder أُدرج هنا (إذ تحتفظ الـ decoders بـ buffers من الـ pool). ولذلك فإن `await using` أعلاه اختياري، ولا ضرر من إبقائه. والاستدعاءات المتكررة الـ sequential تُعيد الـ stream نفسه؛ كما أن النوع غير آمن للاستخدام بشكل concurrent.

راجع [Select\_007\_ResponseCompression.cs](https://github.com/ClickHouse/clickhouse-cs/blob/main/examples/Select/Select_007_ResponseCompression.cs) للاطلاع على مثال قابل للتشغيل.

<h4 id="insert-compression">
  ضغط الإدراج (الطلب)
</h4>

Zstd هو برنامج الترميز `Default` لعمليات الإدراج: تكون القيمة الأولية لـ `InsertOptions.Compressor` هي `ZstdCompressor.Default`،
أي zstd بالمستوى 3. اضبطه على ضاغط آخر لتغيير برنامج الترميز، أو على `null` لإرسال
الـ body دون ضغط.

```csharp theme={null}
var options = new InsertOptions { Compressor = GZipCompressor.Default };  // Content-Encoding: gzip
await client.InsertBinaryAsync("events", columns, rows, options);
```

يأتي الـ driver مزوّدًا بأربعة codecs. لكل منها instance باسم `Default`، ومُنشئ (constructor) يقبل المستوى وحجم الـ write buffer:

| الضاغط | `Content-Encoding` | المُنشئ | `Default` |
| - | - | - | - |
| `ZstdCompressor` | `zstd` | `(int level = 3, int bufferSize = 262144)` | المستوى 3 |
| `Lz4Compressor` | `lz4` | `(Lz4Level level = Lz4Level.Fast, int bufferSize = 262144)` | `Lz4Level.Fast` |
| `GZipCompressor` | `gzip` | `(CompressionLevel level = CompressionLevel.Fastest, int bufferSize = 262144)` | `Fastest` |
| `BrotliCompressor` | `br` | `(CompressionLevel level = CompressionLevel.Fastest, int bufferSize = 262144)` | `Fastest` |

```csharp theme={null}
var options = new InsertOptions { Compressor = new ZstdCompressor(level: 1) };
```

<Note>
  *شارك نسخ الضاغط (instances).* كل `Default` هو نسخة مشتركة واحدة، والضواغط الأربعة جميعها آمنة للاستخدام من عدة
  مسارات تنفيذ (threads) في الوقت نفسه — وهو ما يحدث عندما تكون قيمة
  `InsertOptions.MaxDegreeOfParallelism` أكبر من 1، إذ تستخدم كل عملية insert ضاغطًا واحدًا لكل
  batch. ولا ينفّذ أيٌّ منها `IDisposable`. أنشئ نسختك الخاصة مرة واحدة وأعد استخدامها، بالطريقة
  نفسها التي يُستخدم بها `Default`.
</Note>

<h5 id="custom-compressor">
  codec مخصص
</h5>

الواجهة `IClickHouseCompressor` عامة، ولا يتطلب تنفيذها سوى توفير عضوين اثنين:

```csharp theme={null}
public sealed class MyCompressor : IClickHouseCompressor
{
    public string ContentEncoding => "my-codec";

    public Stream Compress(Stream destination, bool leaveOpen) => /* a compressing write stream */;
}
```

يجب أن يقبل الخادم قيمة `Content-Encoding` التي تحددها. أما بقية الأعضاء —
`Decompress` و`MethodByte` و`MaxEncodedLength` و`Encode` و`Decode` — فلها تطبيقات افتراضية
تُطلق `NotSupportedException`، لذا تجاوز ما يحتاجه codec الخاص بك فقط.
نفّذ `Decompress` لفك ترميز أجسام الاستجابات إلى جانب ضغط الطلبات، وأطلق
`InvalidDataException` من التدفق الذي يُعيده عندما يكون الجسم تالفًا أو بتنسيق خاطئ.

يتحكم `InsertOptions.Compressor` في عمليات الإدراج الثنائية فقط. أما أجسام الطلبات الأخرى في driver فتُضغط وفق قواعد مختلفة، ولا يمر أي منها عبره:

* **كل طلب بنص SQL** (`ExecuteReaderAsync`، `ExecuteScalarAsync`، `ExecuteNonQueryAsync`، `QueryAsync<T>`، `ExecuteRawResultAsync`، طبقة ADO.NET) يرسل عبارته مع `Content-Encoding: gzip` متى كانت قيمة `UseCompression` هي `true` — أي افتراضيًا. وcodec غير قابل للضبط هنا: فـ`AcceptEncoding` يوجّه الاستجابة فقط، وبالتالي فالخيار إما gzip أو لا شيء. أما `Compression=false` فيرسل العبارة دون ضغط. والعبارات صغيرة الحجم، لذا نادرًا ما يستحق الأمر عناء التفكير — لكن من المفيد معرفته عند مراقبة الطلبات عبر proxy أو أثناء packet capture.
* **الجسم متعدد الأجزاء** — أي query تُرسل parameters الخاصة به على هيئة form data (`UseFormDataParameters=true`) — يُرسل دائمًا دون ضغط، أيًا كانت قيمة `UseCompression`.
* **الرفع الخام** (`InsertRawStreamAsync`، `PostStreamAsync`) يعتمد على flag خاص بكل استدعاء، ولا يأخذ في الحسبان `UseCompression` ولا `InsertOptions.Compressor`: gzip عند ضبط الـflag، ودون ضغط فيما عدا ذلك. ولاحظ أن parameter المسمى `useCompression` في `InsertRawStreamAsync` قيمته الافتراضية `true`، لذا يُضغط الرفع الخام بـgzip ما لم تمرر `false` — حتى مع ضبط `Compression=false` على العميل.

***

<h3 id="tuning-compression">
  ضبط الضغط
</h3>

يقايض الضغط استهلاك CPU مقابل توفير البايتات، وما إذا كان ذلك مجديًا يعتمد بشكل شبه كامل على سرعة
وصلتك مقارنةً بسرعة تنفيذ الـ codec. ولا يوجد إعداد واحد يناسب
الجميع.

<h4 id="the-one-number-that-decides-it">
  الرقم الوحيد الذي يحسم الأمر
</h4>

يستحق الضغط العناء طالما أن الـ codec أسرع من الشبكة.

هذا الـ threshold أقل مما يتوقعه معظم الناس في مسار القراءة، لأن ClickHouse يضغط
استجابات HTTP على خيط واحد داخل الـ buffer الخاص بالمخرجات. ووفقًا لقياسات أُجريت على service في ClickHouse Cloud بـ 16 vCPU
(`hits`، RowBinary، المستوى 3)، ينتج الـ server مخرجات مضغوطة بمعدل يتراوح تقريبًا بين 100 و200 ميغابايت/ثانية.

لذا، مع النتائج الكبيرة، وبافتراض معالجة query واحد في كل مرة، تنتفي جدوى الضغط عند حدود 100 ميغابايت/ثانية تقريبًا. وعادةً ما يتجاوز stream واحد من HTTPS
داخل region سحابي واحد هذا الحد، بينما يبقى دونه أي اتصال يعبر الـ public internet أو شبكة VPN أو حدود region.

أما مسار الـ insert فيحتمل الضغط حتى سرعات اتصال أعلى، لأن الـ client لديك يضغط على core مخصص له، وهو عادةً أسرع من ضغط استجابة الـ server.

<h4 id="rough-guide-by-deployment">
  دليل تقريبي حسب النشر
</h4>

| أين يعمل العميل | النطاق الترددي المعتاد | القراءات | عمليات الإدراج |
| - | - | - | - |
| نفس المضيف / loopback | > 500 MB/s | `identity` | `lz4` الأسرع، أو بدون ضغط |
| نفس المنطقة، نفس السحابة | \~100–500 MB/s | `identity` أو `lz4` | `zstd:1` |
| عبر المناطق، نفس السحابة | \~10–100 MB/s | `zstd` | `zstd:3` |
| الإنترنت / VPN / سحابة مختلفة | \< 25 MB/s | `zstd` | `zstd:3` |
| وصلة محدودة الحصة أو مقيّدة جدًا | \< 5 MB/s | `zstd` | `zstd:5`+ أو `br` |

ثلاثة أمور لا يعكسها هذا الجدول:

* **تكلفة الخروج (Egress):** إذا كنت تُحاسَب على نقل البيانات، فللبايتات ثمن يتجاوز زمن الاستجابة، وهذا يدفع نحو ضغط أعلى بغض النظر عن سرعة الوصلة.
* **النتائج الصغيرة:** كل ما سبق يخص الحمولات الكبيرة. أما في الاستجابات الصغيرة فلا يكاد يكون لاختيار الـ codec أثر، ويغلب عليها العبء المصاحب لكل طلب.
* **عمليات الإدراج المتوازية ترفع عتبات الإدراج.** كل رقم إنتاجية أعلاه يخص خيط تنفيذ *واحدًا*. القيمة الافتراضية لـ `InsertOptions.MaxDegreeOfParallelism` هي `1`، لكن رفعها يضغط الدفعات على التوازي، فيتناسب معدل الترميز الإجمالي للعميل تقريبًا مع عدد الأنوية التي تخصصها له. لذا قد يظل الضغط مجديًا لإدراج متوازٍ على وصلة سريعة، عند سرعات تتجاوز بكثير تلك التي يتوقف عندها الضغط عن كونه مجديًا في الإدراج أحادي الخيط. تعامل مع صفوف الإدراج في الجدول باعتبارها *حدًا أدنى*، وإذا كنت تُدرج على دفعات متوازية أصلًا، فأعد الاختبار قبل أن تستنتج أن وصلتك أسرع من أن تستفيد من الضغط.

أما مسار القراءة فلا يتوازى إلا عبر استعلامات متعددة.

<h4 id="choosing-a-codec">
  اختيار الـ codec
</h4>

| Codec | Ratio | استخدمه عندما | Watch out for |
| - | - | - | - |
| `lz4` | الأدنى | وصلات سريعة؛ حين يكون CPU أندر من bandwidth. الأرخص في فك الترميز بفارق كبير، والأسرع مع النتائج الصغيرة — وهو ما يجعله الـ codec الذي تلجأ إليه حين تريد الخروج عن zstd الافتراضي. | لا يحتوي على **مرمّز إنتروبي**، لذا تتخلّف نسبته كثيرًا عن غيره مع البيانات المنحرفة غير المتكررة (سلاسل طويلة من النص الرقمي مثلًا). وهو كذلك الـ codec الأكثر تضررًا من رفع `http_zlib_compression_level`: الانتقال من المستوى 1 إلى 3 يكلّفه نحو 2.7× من CPU مقابل توفير نحو 29% من البايتات فقط. |
| `zstd` | عالية | الخيار العام كلما كانت هناك شبكة حقيقية في المعادلة. أفضل نسبة مقابل CPU في النطاق المهم، وعند المستوى 3 يتفوّق على `lz4` في البايتات *وفي* CPU الخادم *وفي* الزمن الفعلي. | أغلى من `lz4` في **فك الترميز** — 1.6× عند المستوى 3 في قياساتنا، وإن كانا متقاربين عند المستوى 1 — كما أن الـ driver يفك الترميز على thread الاستدعاء لديك. وعند `http_zlib_compression_level=1` تحديدًا يستهلك CPU الخادم *أكثر* قليلًا من `lz4`. |
| `gzip` | متوسطة | التشغيل البيني — مفهوم عالميًا لدى الوسطاء والـ gateways. | يتفوّق عليه كلٌّ من `lz4` و`zstd` في كل المحاور بحسب قياساتنا: أكبر حجمًا من `zstd` بينما يكلّف أضعافًا من CPU في الترميز و5–9× في فك الترميز. اخترْه من أجل التوافقية، لا الأداء. |
| `br` | الأعلى عند المستويات المنخفضة | حين يكون bandwidth هو القيد الحقيقي وبإمكانك إنفاق CPU مقابله. | ينهار أداؤه عند المستويات الأعلى — فعند `http_zlib_compression_level=6` قِسنا استهلاكه لـ CPU الخادم بنحو 3–4× من `zstd`. غير معلَن عنه افتراضيًا، لأنه يتفوّق في الترتيب على كل token احتياطي في القائمة الافتراضية. |

<h4 id="levels">
  المستويات
</h4>

يُتحكَّم في ضغط الاستجابة عبر إعداد خادم واحد هو `http_zlib_compression_level`، وهو ينطبق على *كل* codec في HTTP وليس على zlib وحده. قيمته الافتراضية 3.

لا تغيّره ما لم يكن لديك سبب مبني على قياس فعلي. فوق القيمة الافتراضية، يمنحك تقليصًا ضئيلًا جدًا في الحجم مقابل استهلاك كبير في CPU (فمع `zstd`، الانتقال من 3 إلى 6 يضاعف تقريبًا استهلاك CPU على الخادم مقابل توفير نحو 14% من البايتات فقط)، ويصبح سلوك `br` سيئًا للغاية. أما دون القيمة الافتراضية، عند المستوى 1، فالصورة تختلف فعلًا: يصبح `lz4` أقل كلفة بكثير، ويفقد `zstd` أفضليته عليه من حيث CPU. ويمكنك ضبطه لكل query إن احتجت إلى ذلك:

```csharp theme={null}
using var reader = await client.ExecuteReaderAsync(
    "SELECT * FROM events",
    options: new QueryOptions
    {
        AcceptEncoding = "zstd",
        CustomSettings = new Dictionary<string, object> { ["http_zlib_compression_level"] = 1 },
    });
```

<h4 id="measuring-your-own-crossover">
  قياس نقطة التقاطع لديك
</h4>

أسرع طريقة لتحسين اختيارك لـ codec ومستوى الضغط هي قياس زمن تنفيذ الاستعلام نفسه مع عدد من الـ codecs ثم المقارنة بين النتائج.

```csharp theme={null}
foreach (var codec in new[] { "identity", "lz4", "zstd" })
{
    var sw = Stopwatch.StartNew();
    using var reader = await client.ExecuteReaderAsync(
        "SELECT ... FROM big_table",
        options: new QueryOptions { AcceptEncoding = codec });
    while (await reader.ReadAsync()) { }
    Console.WriteLine($"{codec,-9} {sw.ElapsedMilliseconds} ms");
}
```

وللاطلاع على الجانب الخاص بالخادم من الصورة نفسها، اقرأ `ProfileEvents` من `system.query_log` — واضبط
`QueryOptions.QueryId` حتى تتمكن من العثور على الصف:

```sql theme={null}
SELECT ProfileEvents['UserTimeMicroseconds'] + ProfileEvents['SystemTimeMicroseconds'] AS cpu_us,
       ProfileEvents['NetworkSendBytes'] AS sent_bytes,
       query_duration_ms
FROM system.query_log
WHERE query_id = 'your-query-id' AND type = 'QueryFinish';
```

هناك فخ إذا أجريت هذا الاختبار المرجعي بنفسك: استخدام `LIMIT n` وحده دون `ORDER BY` يُرجع *صفوفاً مختلفة
في كل تشغيل*، لذا يضغط كل تكرار بيانات مختلفة وتصبح النسب مجرد ضوضاء. قارن
دائماً مقابل مجموعة نتائج ثابتة.

***

<h3 id="raw-stream-insert">
  الإدراج من raw stream
</h3>

استخدم `InsertRawStreamAsync` لإدراج البيانات مباشرة من ملف أو من تدفقات الذاكرة بصيغ مثل CSV أو JSON أو Parquet أو أي [صيغة مدعومة في ClickHouse](/ar/reference/formats/index).

**الإدراج من ملف CSV:**

```csharp theme={null}
using var response = await client.InsertRawStreamAsync(
    table: "my_table",
    stream: File.OpenRead("data.csv"),
    format: "CSV",
    columns: ["id", "product", "price"] // Optional: specify columns
);
```

<Warning>
  *يأخذ الـ driver ملكية الـ stream.* تتخلّص `InsertRawStreamAsync` و`PostStreamAsync` من الـ
  stream الذي تمرّره إليها بمجرد انتهاء الطلب، سواء نجح أو فشل. لا تتخلّص منه
  بنفسك ولا تُعِد استخدامه بعد ذلك — ولهذا السبب لا يغلّف المثال أعلاه
  الـ `FileStream` داخل `using`.

  فأي `using` خاص بك سيُنفَّذ بعد أن يكون الـ driver قد تخلّص من الـ stream فعلياً. وبالنسبة إلى `FileStream` أو
  `MemoryStream` يكون هذا الاستدعاء الثاني غير ضار، أما مع stream يُعيد `Dispose` الخاص به buffer مأخوذاً من pool
  أو يُنقص عدّاد المراجع، فسيؤدي ذلك إلى تحرير الـ resource مرتين.

  ولا تنتقل الملكية إلا بعد قبول الوسائط: فإذا أطلق الاستدعاء `ArgumentException` أو
  `ArgumentNullException` بسبب غياب table أو stream أو format، فإن الـ stream يبقى ملكك.
</Warning>

<Note>
  راجع [توثيق إعدادات الـ formats](/ar/reference/settings/formats) للاطلاع على خيارات التحكم في سلوك استيعاب البيانات.
</Note>

***

<h3 id="more-examples">
  المزيد من الأمثلة
</h3>

للاطلاع على المزيد من أمثلة الاستخدام العملية، راجع [دليل الأمثلة](https://github.com/ClickHouse/clickhouse-cs/tree/main/examples) في مستودع GitHub.

<h2 id="ado-net">
  ADO.NET
</h2>

توفّر المكتبة دعمًا كاملًا لـ ADO.NET من خلال `ClickHouseConnection` و`ClickHouseCommand` و`ClickHouseDataReader`. وتُعد واجهة برمجة التطبيقات هذه ضرورية للتكامل مع ORM ‏(Dapper وLinq2db)، وكذلك عند الحاجة إلى طبقات تجريد قواعد البيانات القياسية في .NET.

<h3 id="ado-net-datasource">
  إدارة دورة الحياة باستخدام ClickHouseDataSource
</h3>

**أنشئ الاتصالات دائمًا عبر `ClickHouseDataSource`** لضمان الإدارة السليمة لدورة الحياة وتجميع الاتصالات. يدير DataSource مثيل `ClickHouseClient` واحدًا داخليًا، وتشترك جميع الاتصالات في تجمّع الاتصالات HTTP الخاص به.

```csharp theme={null}
using ClickHouse.Driver.ADO;

// Create DataSource once (register as singleton in DI)
var dataSource = new ClickHouseDataSource("Host=localhost;Username=default;Password=secret");

// Create lightweight connections as needed
await using var connection = await dataSource.OpenConnectionAsync();

// Use the connection
await using var command = connection.CreateCommand("SELECT version()");
var version = await command.ExecuteScalarAsync();
```

في حال استخدام حقن التبعيات:

```csharp theme={null}
// In Startup.cs or Program.cs
services.AddSingleton(sp =>
{
    var factory = sp.GetRequiredService<IHttpClientFactory>();
    return new ClickHouseDataSource("Host=localhost", factory, "ClickHouse");
});

// In your service
public class MyService
{
    private readonly ClickHouseDataSource _dataSource;

    public MyService(ClickHouseDataSource dataSource)
    {
        _dataSource = dataSource;
    }

    public async Task DoWorkAsync()
    {
        await using var connection = await _dataSource.OpenConnectionAsync();
        // Use connection...
    }
}
```

<Warning>
  **لا تُنشئ `ClickHouseConnection` مباشرةً** في شيفرة الإنتاج. فكل إنشاء مباشر له ينشئ عميل HTTP جديدًا وتجمّع اتصالات جديدًا، ما قد يؤدي إلى استنفاد المقابس عند ارتفاع الحمل:

  ```csharp theme={null}
  // لا تفعل هذا — ينشئ مجمّع اتصالات جديدًا في كل مرة
  using var conn = new ClickHouseConnection("Host=localhost");
  await conn.OpenAsync();
  ```

  بدلًا من ذلك، استخدم دائمًا `ClickHouseDataSource` أو شارِك مثيلًا واحدًا من `ClickHouseClient`.
</Warning>

***

<h3 id="ado-net-command">
  استخدام ClickHouseCommand
</h3>

أنشئ أوامر باستخدام اتصال لتنفيذ SQL:

```csharp theme={null}
await using var connection = await dataSource.OpenConnectionAsync();

// Create command with SQL
await using var command = connection.CreateCommand("SELECT * FROM my_table WHERE id = {id:Int64}");
command.AddParameter("id", 42L);

// Execute and read results
await using var reader = await command.ExecuteReaderAsync();
while (reader.Read())
{
    Console.WriteLine($"Name: {reader.GetString("name")}");
}
```

طرق الأوامر:

* `ExecuteNonQueryAsync()` - لعبارات INSERT وUPDATE وDELETE وDDL
* `ExecuteScalarAsync()` - يعيد أول عمود من أول صف
* `ExecuteReaderAsync()` - يعيد `ClickHouseDataReader` للتنقّل بين النتائج

***

<h3 id="ado-net-reader">
  استخدام `ClickHouseDataReader`
</h3>

يتيح `ClickHouseDataReader` الوصول إلى نتائج الاستعلام مع الحفاظ على أنواع البيانات:

```csharp theme={null}
await using var reader = await command.ExecuteReaderAsync();

while (reader.Read())
{
    // Access by column index
    var id = reader.GetInt64(0);
    var name = reader.GetString(1);

    // Access by column name
    var email = reader.GetString("email");

    // Generic access
    var timestamp = reader.GetFieldValue<DateTime>("created_at");

    // Check for null
    if (!reader.IsDBNull("optional_field"))
    {
        var value = reader.GetString("optional_field");
    }
}
```

<h4 id="ado-net-reader-enum-ordinal">
  قراءة الترتيب الرقمي للـ enum
</h4>

يُجسَّد العمود من نوع `Enum8` أو `Enum16` على هيئة الـ label الخاص به: فيُرجع `GetFieldType` النوع `string`، ويمنحك كلٌّ من
`GetString` و`GetValue` و`GetFieldValue<string>` قيمة الـ label. أما الـ accessors الرقمية فتُطلق
الاستثناء `InvalidCastException` عند استخدامها مع عمود enum، لأن القيمة المخزّنة هي سلسلة نصية.

استخدم `TryGetEnumOrdinal` للحصول على الرقم الكامن خلف الـ label:

```csharp theme={null}
if (reader.TryGetEnumOrdinal(ordinal, out int value))
    Console.WriteLine(value);   // e.g. 1 for 'Active' in Enum8('Active' = 1)
```

تُعيد `true` وتضبط `value` في حالة عمود `Enum8`/`Enum16`، وكذلك في حالة عمود `Nullable(Enum...)`
تكون خليته not null. أمّا في حالة خلية NULL أو أي عمود ليس من نوع enum، فتُعيد `false` مع ضبط `value` على `0`.
القيمة الترتيبية هي القيمة signed القادمة من الـ wire، لذا يمكن أن تكون سالبة، كما يمكن أن تتجاوز القيمة الترتيبية لـ `Enum16` حجم
بايت واحد.

<h2 id="best-practices">
  أفضل الممارسات
</h2>

<h3 id="best-practices-connection-lifetime">
  مدة الاتصال وتجميع الاتصالات
</h3>

يستخدم `ClickHouse.Driver` المكوّن `System.Net.Http.HttpClient` في الخلفية. ويحتوي `HttpClient` على تجمّع اتصالات لكل `endpoint`. ونتيجة لذلك:

* تُمرَّر جلسات قاعدة البيانات عبر اتصالات HTTP التي يديرها تجمّع الاتصالات.
* يُعيد التجمّع استخدام اتصالات HTTP تلقائيًا.
* قد تظل الاتصالات مفتوحة حتى بعد التخلّص من الكائنات `ClickHouseClient` أو `ClickHouseConnection`.

**الأنماط الموصى بها:**

| السيناريو | النهج الموصى به |
| - | - |
| الاستخدام العام | استخدم `ClickHouseClient` بنمط singleton |
| ADO.NET / ORMs | استخدم `ClickHouseDataSource` (ينشئ اتصالات تشترك في تجمّع الاتصالات نفسه) |
| بيئات DI | سجّل `ClickHouseClient` أو `ClickHouseDataSource` بنمط singleton مع `IHttpClientFactory` |

<Warning>
  عند استخدام `HttpClient` أو `HttpClientFactory` مخصّص، تأكد من ضبط `PooledConnectionIdleTimeout` على قيمة أقل من `keep_alive_timeout` الخاص بالخادم، لتجنّب الأخطاء الناتجة عن الاتصالات نصف المغلقة. القيمة الافتراضية لـ `keep_alive_timeout` في Cloud deployments هي 10 ثوانٍ.
</Warning>

<Warning>
  تجنّب إنشاء عدة مثيلات من `ClickHouseClient` أو مثيلات `ClickHouseConnection` مستقلة من دون `HttpClient` مشترك. فكل مثيل ينشئ تجمّع الاتصالات الخاص به.
</Warning>

***

<h3 id="best-practice-datetime">
  التعامل مع DateTime
</h3>

1. **استخدم UTC كلما أمكن.** خزّن الطوابع الزمنية في أعمدة `DateTime('UTC')` واستخدم `DateTimeKind.Utc` في الشيفرة الخاصة بك. هذا يزيل أي التباس متعلق بالمنطقة الزمنية.

2. **استخدم `DateTimeOffset` للتعامل الصريح مع المنطقة الزمنية.** فهو يمثّل دائمًا لحظة زمنية محددة ويتضمن معلومات الإزاحة.

3. **حدّد المنطقة الزمنية في تلميحات النوع في SQL.** عند استخدام المعلمات مع قيم DateTime من النوع `Unspecified` والموجّهة إلى أعمدة غير UTC، ضمّن المنطقة الزمنية في SQL:
   ```csharp theme={null}
   var parameters = new ClickHouseParameterCollection();
   parameters.AddParameter("dt", myDateTime);

   await client.ExecuteNonQueryAsync(
       "INSERT INTO table (dt) VALUES ({dt:DateTime('Europe/Amsterdam')})",
       parameters
   );
   ```

***

<h3 id="async-inserts">
  عمليات الإدراج غير المتزامنة
</h3>

تنقل [عمليات الإدراج غير المتزامنة](/ar/concepts/features/operations/insert/asyncinserts) مسؤولية التجميع من العميل إلى الخادم. فبدلًا من اشتراط التجميع من جهة العميل، يخزّن الخادم البيانات الواردة مؤقتًا ثم يفرّغها إلى التخزين وفقًا لعتبات قابلة للتهيئة. ويكون هذا مفيدًا في السيناريوهات عالية التزامن، مثل أحمال عمل observability، حيث يرسل العديد من الوكلاء حمولات صغيرة.

فعِّل عمليات الإدراج غير المتزامنة عبر `CustomSettings` أو سلسلة الاتصال:

```csharp theme={null}
// Using CustomSettings
var settings = new ClickHouseClientSettings("Host=localhost");
settings.CustomSettings["async_insert"] = 1;
settings.CustomSettings["wait_for_async_insert"] = 1; // Recommended: wait for flush acknowledgment

// Or via connection string
// "Host=localhost;set_async_insert=1;set_wait_for_async_insert=1"
```

**وضعان** (يحددهما `wait_for_async_insert`):

| الوضع | السلوك | حالة الاستخدام |
| - | - | - |
| `wait_for_async_insert=1` | تعود عملية الإدراج بعد كتابة البيانات إلى القرص. وتُعاد الأخطاء إلى العميل. | **موصى به** لمعظم أحمال العمل |
| `wait_for_async_insert=0` | تعود عملية الإدراج فورًا عند تخزين البيانات مؤقتًا. لا يوجد ما يضمن حفظ البيانات. | فقط عندما يكون فقدان البيانات مقبولًا |

<Warning>
  مع `wait_for_async_insert=0`، لا تظهر الأخطاء إلا أثناء التفريغ ولا يمكن ربطها بعملية الإدراج الأصلية. كما أن العميل لا يوفّر ضغطًا عكسيًا، مما يعرّض الخادم لخطر زيادة الحمل.
</Warning>

**الإعدادات الأساسية:**

| الإعداد | الوصف |
| - | - |
| `async_insert_max_data_size` | فرّغ عند وصول المخزن المؤقت إلى هذا الحجم (بايت) |
| `async_insert_busy_timeout_ms` | فرّغ بعد انقضاء هذه المهلة (مللي ثانية) |
| `async_insert_max_query_number` | فرّغ بعد تراكم هذا العدد من الاستعلامات |

***

<h3 id="best-practices-sessions">
  الجلسات
</h3>

لا تُفعِّل الجلسات إلا عند الحاجة إلى ميزات من جهة الخادم تحتفظ بالحالة، مثل:

* الجداول المؤقتة (`CREATE TEMPORARY TABLE`)
* الحفاظ على سياق الاستعلام عبر عدة تعليمات
* إعدادات على مستوى الجلسة (`SET max_threads = 4`)

عند تفعيل الجلسات، تُعالَج الطلبات تسلسليًا لمنع الاستخدام المتزامن للجلسة نفسها. ويضيف ذلك حملًا إضافيًا إلى أحمال العمل التي لا تتطلب حالة الجلسة.

```csharp theme={null}
var settings = new ClickHouseClientSettings
{
    Host = "localhost",
    UseSession = true,
    SessionId = "my-session", // Optional -- will be auto-generated if not provided
};

using var client = new ClickHouseClient(settings);

await client.ExecuteNonQueryAsync("CREATE TEMPORARY TABLE temp_ids (id UInt64)");
await client.ExecuteNonQueryAsync("INSERT INTO temp_ids VALUES (1), (2), (3)");

var reader = await client.ExecuteReaderAsync(
    "SELECT * FROM users WHERE id IN (SELECT id FROM temp_ids)"
);
```

**استخدام ADO.NET (للتوافق مع أطر ORM):**

```csharp theme={null}
var settings = new ClickHouseClientSettings
{
    Host = "localhost",
    UseSession = true,
    SessionId = "my-session",
};

var dataSource = new ClickHouseDataSource(settings);
await using var connection = await dataSource.OpenConnectionAsync();

await using var cmd1 = connection.CreateCommand("CREATE TEMPORARY TABLE temp_ids (id UInt64)");
await cmd1.ExecuteNonQueryAsync();

await using var cmd2 = connection.CreateCommand("INSERT INTO temp_ids VALUES (1), (2), (3)");
await cmd2.ExecuteNonQueryAsync();

await using var cmd3 = connection.CreateCommand("SELECT * FROM users WHERE id IN (SELECT id FROM temp_ids)");
await using var reader = await cmd3.ExecuteReaderAsync();
```

***

<h2 id="supported-data-types">
  أنواع البيانات المدعومة
</h2>

يدعم `ClickHouse.Driver` جميع أنواع بيانات ClickHouse. تُبيّن الجداول أدناه أوجه التوافق بين أنواع ClickHouse وأنواع ‎.NET الأصلية عند قراءة البيانات من قاعدة البيانات.

<h3 id="clickhouse-native-type-map-reading">
  تعيين الأنواع: عند القراءة من ClickHouse
</h3>

<h4 id="type-map-reading-integer">
  أنواع الأعداد الصحيحة
</h4>

| نوع ClickHouse | نوع .NET |
| - | - |
| Int8 | `sbyte` |
| UInt8 | `byte` |
| Int16 | `short` |
| UInt16 | `ushort` |
| Int32 | `int` |
| UInt32 | `uint` |
| Int64 | `long` |
| UInt64 | `ulong` |
| Int128 | `BigInteger` |
| UInt128 | `BigInteger` |
| Int256 | `BigInteger` |
| UInt256 | `BigInteger` |

***

<h4 id="type-map-reading-floating-points">
  أنواع الأعداد ذات الفاصلة العائمة
</h4>

| نوع ClickHouse | نوع .NET |
| - | - |
| Float32 | `float` |
| Float64 | `double` |
| BFloat16 | `float` |

***

<h4 id="type-map-reading-decimal">
  الأنواع العشرية
</h4>

| نوع ClickHouse | نوع .NET |
| - | - |
| Decimal(P, S) | `decimal` / `ClickHouseDecimal` |
| Decimal32(S) | `decimal` / `ClickHouseDecimal` |
| Decimal64(S) | `decimal` / `ClickHouseDecimal` |
| Decimal128(S) | `decimal` / `ClickHouseDecimal` |
| Decimal256(S) | `decimal` / `ClickHouseDecimal` |

<Note>
  يُتحكَّم في تحويل النوع Decimal من خلال الإعداد UseCustomDecimals.
</Note>

***

<h4 id="type-map-reading-boolean">
  نوع Boolean
</h4>

| نوع ClickHouse | نوع .NET |
| - | - |
| Bool | `bool` |

***

<h4 id="type-map-reading-strings">
  أنواع السلاسل النصية
</h4>

| نوع ClickHouse | نوع .NET |
| - | - |
| String | `string` |
| FixedString(N) | `string` |

<Note>
  افتراضيًا، يُرجَع كلٌّ من العمودين `String` و`FixedString(N)` على هيئة `string`. عيّن `ReadStringsAsByteArrays=true` في سلسلة الاتصال لقراءتهما على هيئة `byte[]` بدلًا من ذلك. يفيد هذا عند تخزين بيانات ثنائية قد لا تكون بتنسيق UTF-8 صالح.

  يمتد أثر هذا الإعداد إلى السلاسل النصية المتداخلة داخل الأنواع الأخرى أيضًا، فتُقرأ `Array(String)` على هيئة `byte[][]`
  وتُقرأ `Map(String, String)` على هيئة `Dictionary<byte[], byte[]>` — بما في ذلك المفاتيح. الاستثناء الوحيد هو
  عمود `JSON`، إذ تبقى أوراقه النصية نصًا دائمًا؛ انظر [نوع JSON](#type-map-reading-json).
</Note>

***

<h4 id="type-map-reading-datetime">
  أنواع التاريخ والوقت
</h4>

| نوع ClickHouse | نوع .NET |
| - | - |
| Date | `DateTime` |
| Date32 | `DateTime` |
| DateTime | `DateTime` |
| DateTime32 | `DateTime` |
| DateTime64 | `DateTime` |
| Time | `TimeSpan` |
| Time64 | `TimeSpan` |

يخزّن ClickHouse قيم `DateTime` و`DateTime64` داخليًا على هيئة طوابع زمنية Unix (بالثواني أو بوحدات أصغر من الثانية منذ حقبة Unix). وعلى الرغم من أن التخزين يكون دائمًا بتوقيت UTC، فقد تكون للأعمدة منطقة زمنية مرتبطة بها تؤثر في كيفية عرض القيم وتفسيرها.

عند قراءة قيم `DateTime`، تُضبط الخاصية `DateTime.Kind` بناءً على المنطقة الزمنية للعمود:

| تعريف العمود | قيمة `DateTime.Kind` المُعادة | ملاحظات |
| - | - | - |
| `DateTime('UTC')` | `Utc` | منطقة UTC زمنية محددة صراحةً |
| `DateTime('Europe/Amsterdam')` | `Unspecified` | تُطبَّق الإزاحة |
| `DateTime` | `Unspecified` | يُحفَظ الوقت المحلي كما هو |

بالنسبة إلى الأعمدة التي لا تستخدم UTC، تمثل قيمة `DateTime` المُعادة الوقت المحلي في تلك المنطقة الزمنية. استخدم `ClickHouseDataReader.GetDateTimeOffset()` للحصول على `DateTimeOffset` مع الإزاحة الصحيحة لتلك المنطقة الزمنية:

```csharp theme={null}
var reader = (ClickHouseDataReader)await connection.ExecuteReaderAsync(
    "SELECT toDateTime('2024-06-15 14:30:00', 'Europe/Amsterdam')");
reader.Read();

var dt = reader.GetDateTime(0);    // 2024-06-15 14:30:00, Kind=Unspecified
var dto = reader.GetDateTimeOffset(0); // 2024-06-15 14:30:00 +02:00 (CEST)
```

بالنسبة إلى الأعمدة **التي لا تتضمن** منطقة زمنية صريحة (أي `DateTime` بدلًا من `DateTime('Europe/Amsterdam')`)، يعيد برنامج التشغيل قيمة `DateTime` مع `Kind=Unspecified`. وهذا يحافظ على الوقت المحلي كما تظهره الساعة تمامًا كما هو مخزّن، من دون افتراض أي منطقة زمنية.

إذا كنت بحاجة إلى سلوكٍ مدركٍ للمنطقة الزمنية للأعمدة التي لا تتضمن مناطق زمنية صريحة، فإما أن:

1. تستخدم مناطق زمنية صريحة في تعريفات الأعمدة: `DateTime('UTC')` أو `DateTime('Europe/Amsterdam')`
2. تطبّق المنطقة الزمنية بنفسك بعد القراءة.

***

<h4 id="type-map-reading-json">
  نوع JSON
</h4>

| نوع ClickHouse | نوع .NET | ملاحظات |
| - | - | - |
| Json | `JsonObject` | الافتراضي (`JsonReadMode=Binary`) |
| Json | `string` | عند استخدام `JsonReadMode=String` |

يُحدَّد نوع الإرجاع لأعمدة JSON من خلال الإعداد `JsonReadMode`:

* **`Binary` (الافتراضي)**: يعيد `System.Text.Json.Nodes.JsonObject`. يوفّر وصولًا منظّمًا إلى بيانات JSON، لكن أنواع ClickHouse المتخصصة (مثل عناوين IP وUUIDs والقيم العشرية الكبيرة) تُحوَّل إلى تمثيلاتها النصية داخل بنية JSON.

* **`String`**: يعيد JSON الخام كسلسلة `string`. ويحافظ على تمثيل JSON كما هو تمامًا من ClickHouse، وهو ما يكون مفيدًا عندما تحتاج إلى تمرير JSON كما هو دون تحليله، أو عندما تريد التعامل مع فك التسلسل بنفسك.

```csharp theme={null}
// Configure string mode via settings
var settings = new ClickHouseClientSettings("Host=localhost")
{
    JsonReadMode = JsonReadMode.String
};

// Or via connection string
// "Host=localhost;JsonReadMode=String"
```

`None` هو وضع ثالث. يقرأ البيانات تمامًا كما يفعل `Binary`، لكنه لا يرسل أي server setting مع الـ query — استخدمه مع connection غير مسموح لها بتعيين أي منها.

<h5 id="type-map-reading-json-nulls">
  المسارات محددة النوع وقيم null
</h5>

المسار المُعلَن في نوع العمود هو **مسار محدد النوع**؛ أما أي مسار آخر في المستند فهو
**مسار ديناميكي**. ويختلف النوعان عندما تكون القيمة null.

يظهر المسار محدد النوع دائمًا في `JsonObject`. وإذا أُعلن على أنه `Nullable(T)` أو `Dynamic`، فإنه يُعاد
بقيمة JSON null سواء كانت القيمة المخزنة null أو لم يتضمّن المستند هذا المسار أصلًا — ولا يمكن التمييز بين
الحالتين:

```csharp theme={null}
// Column type JSON(x Nullable(Int64))
// stored '{"x":null}'  ->  {"x":null}
// stored '{}'          ->  {"x":null}
```

عند التصريح عن المسار بنوع غير قابل للقيم الفارغة (non-nullable)، يأخذ المسار الغائب القيمة الافتراضية لذلك النوع — فـ `JSON(x String)`
يعطي `{"x":""}`، و`JSON(x Int64)` يعطي `{"x":0}`.

أما المسار الديناميكي الذي تكون قيمته null فيُحذف من الكائن بالكامل، لذا تُرجع `ContainsKey` القيمة
false بشأنه. وقراءة `{"x":null}` من عمود `JSON` عادي تعطي `{}`.

أما المسارات المحددة النوع المتداخلة فتُنشئ العناصر الأصلية الخاصة بها، لذا يُنتج `JSON(a.b Nullable(Int64))` القيمة `{"a":{"b":null}}`
حتى مع مستند فارغ.

<Note>
  هذا ما يعرضه الـ server نفسه، لذا أصبح وضعا `Binary` و`String` متطابقين الآن. قبل الإصدار 1.4.0 كان
  المسار المحدد النوع الذي يحمل null يُحذف من `JsonObject`، ما جعل `{"x":null}` تُقرأ على أنها
  `{}` — وفي حالة مسار متداخل مثل `JSON(a.b Nullable(Int64))` كانت شجرة `a` الفرعية بأكملها تختفي.
</Note>

<h5 id="type-map-reading-json-strings">
  السلاسل النصية داخل عمود JSON
</h5>

تُعاد الأوراق النصية داخل عمود `JSON` دائمًا كنص، أيًا كانت قيمة
`ReadStringsAsByteArrays`، إذ لا يملك `JsonValue` صيغة مصفوفة بايتات، ومن ثمّ فإن `byte[]` سيظهر
بترميز base64. وينطبق ذلك على `String` و`FixedString`، وعلى ما يُغلَّف منها بـ
`LowCardinality` أو `Nullable` أو `SimpleAggregateFunction`، وعلى السلاسل النصية داخل `Array` و`Map`،
بما في ذلك مفاتيح الخريطة.

<Note>
  أما مصفوفة البايتات التي يتعذّر على قارئ JSON معرفة نوعها فتظهر فعلًا بترميز base64: فالمسار
  من النوع `Variant` أو `Dynamic` يحمل قيمةً لا يُعرف نوعها إلا على مستوى كل صف، لذا فإن سلسلة نصية
  ضمن `Variant(Array(UInt8), String)` تُعاد مُرمَّزة بـ base64. وهذا لا يتغير في كلا الإعدادين.

  أما نوع مفتاح خريطة JSON الذي ليس `String` تمامًا — مثل `Map(LowCardinality(String), String)` —
  فيُطلق `NotSupportedException`.
</Note>

<h5 id="overlapping-paths">
  المسارات المتداخلة
</h5>

يقبل ClickHouse عمودًا يعلن مسارًا كقيمة وكأصل لمسار آخر في آنٍ واحد، على سبيل المثال `JSON(a Int64, a.b Int64)`. وبما أن كلا المسارين موجود في كل صف، فإن الخادم يعرض الصف بمفتاح مكرر: `{"a":0,"a":{"b":7}}`. ولا يمكن لـ `JsonObject` أن يحمل قيمتين لمفتاح واحد، لذا يطرح `JsonReadMode.Binary` استثناء `SerializationException` يذكر فيه المسارين. وينطبق الأمر نفسه عندما تكون القيمة من نوع `Map`، كما في `JSON(a Map(String, Int64))` المقروء من صف يحتوي أيضًا على `a.b` ديناميكي.

ولا ينطبق ذلك إلا عندما يحمل الطرفان قيمة في الصف نفسه. أما الطرف الذي لا يحمل شيئًا — سواء كان `NULL`، أو كائنًا فارغًا، أو شجرة فرعية جميع قيمها `NULL` — فيفسح المجال للطرف الذي يحمل البيانات، أيًّا كان المسار الذي يرسله الخادم أولًا. وعليه فإن التداخل المُعلن بأنواع `Nullable` يملأ طرفًا واحدًا في كل صف ويُقرأ دون خطأ: إذ يعطي `JSON(a Nullable(Int64), a.b Nullable(Int64))` النتيجتين `{"a":5}` و`{"a":{"b":7}}` كما هو متوقع.

اقرأ هذا النوع من الأعمدة باستخدام `JsonReadMode.String` للحصول على نص JSON كما أرسله الخادم دون تغيير، بما في ذلك المفتاح المكرر.

اضبط `AllowDuplicateJsonKeys` لمواصلة قراءة العمود كـ `JsonObject` بدلًا من طرح استثناء. عندئذٍ يحتفظ الـ driver بآخر القيمتين ورودًا في الصف ويُسقط الأخرى، فتكون النتيجة منقوصة: إذ يُقرأ `JSON(a Int64, a.b Int64)` الذي يحمل `{"a.b":7}` على أنه `{"a":0}`. أما المسار الذي يحمل قيمة ويحمل أصله قيمة scalar أو مصفوفة فيظل يطرح استثناءً، لأنه لا يمكن وضع شجرة فرعية تحت أيٍّ منهما.

```csharp theme={null}
var settings = new ClickHouseClientSettings("Host=localhost")
{
    AllowDuplicateJsonKeys = true
};

// Or via connection string
// "Host=localhost;AllowDuplicateJsonKeys=true"
```

***

<h4 id="type-map-reading-map">
  Map type
</h4>

| ClickHouse Type | .NET Type | Notes |
| - | - | - |
| Map(K, V) | `Dictionary<K, V>` | الافتراضي (`MapReadMode=Dictionary`) |
| Map(K, V) | `List<KeyValuePair<K, V>>` | عند `MapReadMode=KeyValuePairs` |

النوع `Map(K, V)` في ClickHouse هو فعليًا `Array(Tuple(K, V))`، ويمكنه أن يضم عدة مدخلات تحمل المفتاح نفسه، وهو ما لا يتيحه `Dictionary`. لذلك، في الوضع الافتراضي، لا يحتفظ المفتاح المتكرر إلا بقيمته الأخيرة وتُسقَط الأزواج السابقة. ويحدد الإعداد `MapReadMode` التمثيل المستخدم:

* **`Dictionary` (الافتراضي)**: يُرجع `Dictionary<K, V>`.

* **`KeyValuePairs`**: يُرجع `List<KeyValuePair<K, V>>` بالترتيب الذي أرسل به الخادم الأزواج، فيُحتفظ بكل زوج، بما في ذلك المدخلات التي تتكرر فيها المفاتيح.

```csharp theme={null}
// Configure key-value-pair mode via settings
var settings = new ClickHouseClientSettings("Host=localhost")
{
    MapReadMode = MapReadMode.KeyValuePairs
};

// Or via connection string
// "Host=localhost;MapReadMode=KeyValuePairs"
```

يحدد الوضع نوع إطار العمل المستخدم لعمود `Map`، لذا فهو ينطبق أيضًا على `GetFieldValue<T>`، وعلى أنواع الـ schema التي يُبلغ عنها الـ driver، وعلى تعيين خصائص POCO. كما ينطبق أينما ظهر الـ map في شجرة نوع العمود — بما في ذلك `Array(Map(...))` و`Map(K, Map(...))` و`Tuple(..., Map(...))` و`Dynamic`.

كلا التمثيلين مقبول في مسار الكتابة في أي من الوضعين — راجع [كتابة maps](#type-map-writing-other).

***

<h4 id="type-map-reading-other">
  أنواع أخرى
</h4>

| نوع ClickHouse | نوع .NET |
| - | - |
| UUID | `Guid` |
| IPv4 | `IPAddress` |
| IPv6 | `IPAddress` |
| Nothing | `DBNull` |
| Dynamic | انظر الملاحظة |
| Array(T) | `T[]` (تُقرأ `Array(Array(T))` المتداخلة على أنها `T[][]` غير منتظمة؛ استخدم `reader.GetFieldValue<T[,]>(ordinal)` لمادية البيانات المستطيلة كمصفوفة CLR متعددة الأبعاد) |
| Tuple(T1, T2, ...) | `Tuple<T1, T2, ...>` / `LargeTuple` |
| Map(K, V) | `Dictionary<K, V>`، أو `List<KeyValuePair<K, V>>` عند `MapReadMode=KeyValuePairs` — انظر [Map type](#type-map-reading-map) |
| Nullable(T) | `T?` |
| Enum8 | `string` |
| Enum16 | `string` |
| LowCardinality(T) | نفس T |
| SimpleAggregateFunction | نفس النوع الأساسي |
| Nested(...) | `Tuple[]` |
| Variant(T1, T2, ...) | انظر الملاحظة |
| QBit(T, dimension) | `T[]` |

<Note>
  سيُحوَّل النوعان Dynamic وVariant إلى النوع المقابل للنوع الأساسي الفعلي في كل صف.
</Note>

***

<h4 id="type-map-reading-geometry">
  أنواع Geometry
</h4>

| نوع ClickHouse | نوع .NET |
| - | - |
| Point | `Tuple<double, double>` |
| Ring | `Tuple<double, double>[]` |
| LineString | `Tuple<double, double>[]` |
| Polygon | `Ring[]` |
| MultiLineString | `LineString[]` |
| MultiPolygon | `Polygon[]` |
| Geometry | راجع الملاحظة |

<Note>
  نوع Geometry هو نوع Variant يمكن أن يحتوي على أيٍّ من أنواع Geometry. وسيُحوَّل إلى النوع المقابل.
</Note>

***

<h3 id="clickhouse-native-type-map-writing">
  تعيين الأنواع: الكتابة إلى ClickHouse
</h3>

عند إدراج البيانات، يحوّل برنامج التشغيل أنواع .NET إلى أنواع ClickHouse المقابلة لها. وتبيّن الجداول أدناه أنواع .NET المقبولة لكل نوع عمود في ClickHouse.

<h4 id="type-map-writing-integer">
  أنواع الأعداد الصحيحة
</h4>

| نوع ClickHouse | أنواع .NET المقبولة | ملاحظات |
| - | - | - |
| Int8 | `sbyte`، وأي نوع متوافق مع `Convert.ToSByte()` | |
| UInt8 | `byte`، وأي نوع متوافق مع `Convert.ToByte()` | |
| Int16 | `short`، وأي نوع متوافق مع `Convert.ToInt16()` | |
| UInt16 | `ushort`، وأي نوع متوافق مع `Convert.ToUInt16()` | |
| Int32 | `int`، وأي نوع متوافق مع `Convert.ToInt32()` | |
| UInt32 | `uint`، وأي نوع متوافق مع `Convert.ToUInt32()` | |
| Int64 | `long`، وأي نوع متوافق مع `Convert.ToInt64()` | |
| UInt64 | `ulong`، وأي نوع متوافق مع `Convert.ToUInt64()` | |
| Int128 | `BigInteger`، `decimal`، `double`، `float`، `int`، `uint`، `long`، `ulong`، وأي نوع متوافق مع `Convert.ToInt64()` | |
| UInt128 | `BigInteger`، `decimal`، `double`، `float`، `int`، `uint`، `long`، `ulong`، وأي نوع متوافق مع `Convert.ToInt64()` | |
| Int256 | `BigInteger`، `decimal`، `double`، `float`، `int`، `uint`، `long`، `ulong`، وأي نوع متوافق مع `Convert.ToInt64()` | |
| UInt256 | `BigInteger`، `decimal`، `double`، `float`، `int`، `uint`، `long`، `ulong`، وأي نوع متوافق مع `Convert.ToInt64()` | |

***

<h4 id="type-map-writing-floating-point">
  أنواع الأعداد ذات الفاصلة العائمة
</h4>

| نوع ClickHouse | أنواع .NET المقبولة | ملاحظات |
| - | - | - |
| Float32 | `float`، وأي نوع متوافق مع `Convert.ToSingle()` | |
| Float64 | `double`، وأي نوع متوافق مع `Convert.ToDouble()` | |
| BFloat16 | `float`، وأي نوع متوافق مع `Convert.ToSingle()` | يُقتطع إلى تنسيق bfloat16 ‏(brain float)‏ ذي 16 بت |

***

<h4 id="type-map-writing-boolean">
  النوع المنطقي
</h4>

| نوع ClickHouse | أنواع .NET المقبولة | ملاحظات |
| - | - | - |
| Bool | `bool` | |

***

<h4 id="type-map-writing-strings">
  أنواع String
</h4>

| نوع ClickHouse | أنواع .NET المقبولة | ملاحظات |
| - | - | - |
| String | `string`, `byte[]`, `ReadOnlyMemory<byte>`, `Stream` | تُكتب الأنواع الثنائية مباشرةً؛ ويمكن أن تكون التدفقات قابلةً لـ seek أو غير قابلة له |
| FixedString(N) | `string`, `byte[]`, `ReadOnlyMemory<byte>`, `Stream` | يُرمَّز String بترميز UTF-8 ويُحشّى؛ ويجب أن تكون الأنواع الثنائية بطول N بايتات بالضبط |

***

<h4 id="type-map-writing-datetime">
  أنواع التاريخ والوقت
</h4>

| نوع ClickHouse | أنواع .NET المقبولة | ملاحظات |
| - | - | - |
| Date | `DateTime`, `DateTimeOffset`, `DateOnly`, أنواع NodaTime | تُحوَّل إلى أيام Unix بصيغة UInt16؛ والنطاق المدعوم هو `[1970-01-01, 2149-06-06]` |
| Date32 | `DateTime`, `DateTimeOffset`, `DateOnly`, أنواع NodaTime | تُحوَّل إلى أيام Unix بصيغة Int32؛ والنطاق المدعوم هو `[1900-01-01, 2299-12-31]` |
| DateTime | `DateTime`, `DateTimeOffset`, `DateOnly`, أنواع NodaTime | انظر أدناه للاطّلاع على التفاصيل؛ والنطاق المدعوم هو `[1970-01-01, 2106-02-07 06:28:15]` UTC |
| DateTime32 | `DateTime`, `DateTimeOffset`, `DateOnly`, أنواع NodaTime | مثل DateTime |
| DateTime64 | `DateTime`, `DateTimeOffset`, `DateOnly`, أنواع NodaTime | تعتمد الدقة على معلمة Scale |
| Time | `TimeSpan`, `TimeOnly`, `int` | تُقيَّد إلى ±999:59:59؛ ويُتعامل مع `int` على أنه ثوانٍ |
| Time64 | `TimeSpan`, `TimeOnly`, `decimal`, `double`, `float`, `int`, `long`, `string` | تُحلَّل السلسلة النصية بالصيغة `[-]HHH:MM:SS[.fraction]`؛ وتُقيَّد إلى ±999:59:59.999999999 |

<Note>
  **القيم خارج النطاق**

  في مسار الكتابة الثنائي، تؤدي قيم `Date` و`Date32` و`DateTime` و`DateTime32` الواقعة خارج نطاقها المدعوم إلى إطلاق `ArgumentOutOfRangeException` عند `Write`، مع ذكر نوع العمود والنطاق المدعوم. في السابق، كان يمكن اقتطاع القيم خارج النطاق بصمت عبر عدد صحيح 32-بت ثم يعيد الخادم تفسيرها، مما ينتج عنه طوابع زمنية حقيقية لكنها غير صحيحة.
</Note>

يراعي برنامج التشغيل `DateTime.Kind` عند كتابة القيم:

| DateTime.Kind | معلمات HTTP | Bulk |
| - | - | - |
| Utc | تُحفَظ اللحظة الزمنية كما هي | تُحفَظ اللحظة الزمنية كما هي |
| Local | تُحفَظ اللحظة الزمنية كما هي | تُحفَظ اللحظة الزمنية كما هي |
| Unspecified | يُتعامل معه كوقت محلي في المنطقة الزمنية لنوع المعلمة (UTC افتراضيًا) | يُتعامل معه كوقت محلي في المنطقة الزمنية للعمود |

تحافظ قيم `DateTimeOffset` دائمًا على اللحظة الزمنية الدقيقة.

**مثال: DateTime بتوقيت UTC (تُحفَظ اللحظة الزمنية كما هي)**

```csharp theme={null}
var utcTime = new DateTime(2024, 1, 15, 12, 0, 0, DateTimeKind.Utc);
// Stored as 12:00 UTC
// Read from DateTime('Europe/Amsterdam') column: 13:00 (UTC+1)
// Read from DateTime('UTC') column: 12:00 UTC
```

**مثال: DateTime غير محدد (التوقيت المحلي)**

```csharp theme={null}
var wallClock = new DateTime(2024, 1, 15, 14, 30, 0, DateTimeKind.Unspecified);
// Written to DateTime('Europe/Amsterdam') column: stored as 14:30 Amsterdam time
// Read back from DateTime('Europe/Amsterdam') column: 14:30
```

**التوصية:** للحصول على أبسط سلوك وأكثره قابلية للتنبؤ، استخدم `DateTimeKind.Utc` أو `DateTimeOffset` في جميع عمليات DateTime. يضمن ذلك أن تعمل شيفرتك بشكل متسق بغض النظر عن المنطقة الزمنية للخادم أو العميل أو العمود.

<h4 id="datetime-http-param-vs-bulkcopy">
  معلمات HTTP مقابل Bulk Copy
</h4>

يوجد فرق مهم بين ربط معلمات HTTP وBulk Copy عند كتابة قيم DateTime من النوع `Unspecified`:

**Bulk Copy** يعرف المنطقة الزمنية للعمود المستهدف ويفسّر قيم `Unspecified` بشكل صحيح وفقًا لتلك المنطقة الزمنية.

**HTTP Parameters** لا تعرف تلقائيًا المنطقة الزمنية للعمود. يجب تحديدها في تلميح نوع SQL:

```csharp theme={null}
// CORRECT: Timezone in SQL type hint - type is extracted automatically
command.CommandText = "INSERT INTO table (dt_amsterdam) VALUES ({dt:DateTime('Europe/Amsterdam')})";
command.AddParameter("dt", myDateTime);

// INCORRECT: Without timezone hint, interpreted as UTC
command.CommandText = "INSERT INTO table (dt_amsterdam) VALUES ({dt:DateTime})";
command.AddParameter("dt", myDateTime);
// String value "2024-01-15 14:30:00" interpreted as UTC, not Amsterdam time!
```

| `DateTime.Kind` | العمود المستهدف | معلمة HTTP (مع تلميح المنطقة الزمنية) | معلمة HTTP (من دون تلميح المنطقة الزمنية) | النسخ المجمّع |
| - | - | - | - | - |
| `Utc` | UTC | تبقى اللحظة الزمنية كما هي | تبقى اللحظة الزمنية كما هي | تبقى اللحظة الزمنية كما هي |
| `Utc` | Europe/Amsterdam | تبقى اللحظة الزمنية كما هي | تبقى اللحظة الزمنية كما هي | تبقى اللحظة الزمنية كما هي |
| `Local` | أيّ قيمة | تبقى اللحظة الزمنية كما هي | تبقى اللحظة الزمنية كما هي | تبقى اللحظة الزمنية كما هي |
| `Unspecified` | UTC | يُعامَل على أنه UTC | يُعامَل على أنه UTC | يُعامَل على أنه UTC |
| `Unspecified` | Europe/Amsterdam | يُعامَل على أنه بتوقيت Amsterdam | **يُعامَل على أنه UTC** | يُعامَل على أنه بتوقيت Amsterdam |

***

<h4 id="type-map-writing-decimal">
  أنواع Decimal
</h4>

| نوع ClickHouse | أنواع .NET المقبولة | ملاحظات |
| - | - | - |
| Decimal(P,S) | `decimal`، `ClickHouseDecimal`، وأي نوع متوافق مع `Convert.ToDecimal()` | يتم طرح `OverflowException` إذا تجاوزت القيمة الدقة |
| Decimal32 | `decimal`، `ClickHouseDecimal`، وأي نوع متوافق مع `Convert.ToDecimal()` | الحد الأقصى للدقة هو 9 |
| Decimal64 | `decimal`، `ClickHouseDecimal`، وأي نوع متوافق مع `Convert.ToDecimal()` | الحد الأقصى للدقة هو 18 |
| Decimal128 | `decimal`، `ClickHouseDecimal`، وأي نوع متوافق مع `Convert.ToDecimal()` | الحد الأقصى للدقة هو 38 |
| Decimal256 | `decimal`، `ClickHouseDecimal`، وأي نوع متوافق مع `Convert.ToDecimal()` | الحد الأقصى للدقة هو 76 |

***

<h4 id="type-map-writing-json">
  نوع JSON
</h4>

| ClickHouse Type | أنواع .NET المقبولة | ملاحظات |
| - | - | - |
| Json | `string`, `JsonObject`, `JsonNode`, أي كائن | يعتمد السلوك على إعداد `JsonWriteMode` |

يتحكم إعداد `JsonWriteMode` في السلوك عند كتابة JSON:

| نوع الإدخال | `JsonWriteMode.String` (الافتراضي) | `JsonWriteMode.Binary` |
| - | - | - |
| `string` | يُمرَّر كما هو | يطرح `ArgumentException` |
| `JsonObject` | يُسلسَل عبر `ToJsonString()` | يطرح `ArgumentException` |
| `JsonNode` | يُسلسَل عبر `ToJsonString()` | يطرح `ArgumentException` |
| POCO مسجّل | يُسلسَل عبر `JsonSerializer.Serialize()` | ترميز ثنائي مع دعم تلميحات النوع وسمات المسار المخصصة |
| POCO غير مسجّل / كائن مجهول | يُسلسَل عبر `JsonSerializer.Serialize()` | يطرح `ClickHouseJsonSerializationException` |

* **`String` (الافتراضي)**: يقبل `string` و`JsonObject` و`JsonNode` أو أي كائن. تُسلسَل جميع المدخلات عبر `System.Text.Json.JsonSerializer` وتُرسل كسلاسل JSON لتحليلها على جهة الخادم. هذا هو الوضع الأكثر مرونة ويعمل من دون تسجيل النوع.

* **`Binary`**: لا يقبل إلا أنواع POCO المسجّلة. تُحوَّل البيانات إلى تنسيق JSON الثنائي الخاص بـ ClickHouse على جهة العميل مع دعم كامل لتلميحات النوع. ويتطلب استدعاء `connection.RegisterJsonSerializationType<T>()` قبل الاستخدام. وتؤدي كتابة قيم `string` أو `JsonNode` في هذا الوضع إلى طرح `ArgumentException`.

```csharp theme={null}
// Default String mode works with any input
await client.InsertBinaryAsync(
    "my_table",
    new[] { "id", "data" },
    new[] { new object[] { 1u, new { name = "test", value = 42 } } }
);

// Binary mode requires explicit opt-in and type registration
var settings = new ClickHouseClientSettings("Host=localhost")
{
    JsonWriteMode = JsonWriteMode.Binary
};
using var client = new ClickHouseClient(settings);
client.RegisterJsonSerializationType<MyPocoType>();
```

<h5 id="json-typed-columns">
  أعمدة JSON محددة الأنواع
</h5>

عندما يحتوي عمود JSON على تلميحات للأنواع (مثل `JSON(id UInt64, price Decimal128(2))`)، يستخدم برنامج التشغيل هذه التلميحات لتسلسل القيم مع الحفاظ الكامل على سلامة الأنواع. وهذا يحافظ على الدقة في أنواع مثل `UInt64` و`Decimal` و`UUID` و`DateTime64`، والتي قد تفقد دقتها لولا ذلك عند تسلسلها بصيغة JSON عامة.

<h5 id="json-poco-serialization">
  تسلسل كائنات POCO
</h5>

يمكن كتابة كائنات POCO في أعمدة JSON بطريقتين وفقًا لـ `JsonWriteMode`:

**String mode (الافتراضي)**: تُسلسَل كائنات POCO عبر `System.Text.Json.JsonSerializer`. لا يتطلب ذلك تسجيل الأنواع. هذا هو النهج الأبسط، كما أنه يعمل مع الكائنات المجهولة.

**Binary mode**: تُسلسَل كائنات POCO باستخدام تنسيق JSON الثنائي الخاص ببرنامج التشغيل مع دعم كامل لتلميحات النوع. يجب تسجيل الأنواع باستخدام `connection.RegisterJsonSerializationType<T>()` قبل الاستخدام. يدعم هذا الوضع تعيينات مسارات مخصّصة عبر السمات:

* **`[ClickHouseJsonPath("path")]`**: يربط خاصيةً بمسار JSON مخصّص. ويكون ذلك مفيدًا مع البُنى المتداخلة أو عندما يختلف اسم الخاصية عن مفتاح JSON المطلوب. **يعمل فقط في Binary mode.**

* **`[ClickHouseJsonIgnore]`**: يستبعد خاصيةً من التسلسل. **يعمل فقط في Binary mode.**

```sql theme={null}
CREATE TABLE events (
    id UInt32,
    data JSON(`user.id` Int64, `user.name` String, Timestamp DateTime64(3))
) ENGINE = MergeTree() ORDER BY id
```

```csharp theme={null}
using ClickHouse.Driver.Json;

public class UserEvent
{
    [ClickHouseJsonPath("user.id")]
    public long UserId { get; set; }

    [ClickHouseJsonPath("user.name")]
    public string UserName { get; set; }

    public DateTime Timestamp { get; set; }

    [ClickHouseJsonIgnore]
    public string InternalData { get; set; }  // Not serialized
}

// For Binary mode: Register the type and enable Binary mode
var settings = new ClickHouseClientSettings("Host=localhost") { JsonWriteMode = JsonWriteMode.Binary };
using var client = new ClickHouseClient(settings);
client.RegisterJsonSerializationType<UserEvent>();

// Insert POCO - serialized to JSON with nested structure via custom path attributes
await client.InsertBinaryAsync(
    "events",
    new[] { "id", "data" },
    new[] { new object[] { 1u, new UserEvent { UserId = 123, UserName = "Alice", Timestamp = DateTime.UtcNow } } }
);
// Resulting JSON: {"user": {"id": 123, "name": "Alice"}, "Timestamp": "2024-01-15T..."}
```

مطابقة أسماء الخصائص مع تلميحات نوع العمود حسّاسة لحالة الأحرف. فالخاصية `UserId` لن تتطابق إلا مع تلميح مُعرَّف باسم `UserId`، وليس `userid`. وهذا يتوافق مع سلوك ClickHouse الذي يسمح بوجود مسارات مثل `userName` و`UserName` معًا كحقول منفصلة.

**القيود (في Binary mode فقط):**

* يجب تسجيل أنواع POCO على الاتصال باستخدام `connection.RegisterJsonSerializationType<T>()` قبل إجراء التسلسل. وستؤدي محاولة تسلسل نوع غير مسجّل إلى ظهور الاستثناء `ClickHouseJsonSerializationException`.
* تتطلب خصائص القاموس والمصفوفة/القائمة تلميحات نوع في تعريف العمود لكي تُسلسَل بشكل صحيح. ومن دون هذه التلميحات، استخدم String mode بدلًا من ذلك.
* لا تُكتَب القيم الخالية في خصائص POCO إلا إذا كان للمسار تلميح نوع `Nullable(T)` في تعريف العمود. ولا يسمح ClickHouse بأنواع `Nullable` داخل مسارات JSON الديناميكية، لذلك يتم تخطي الخصائص الخالية غير المزوّدة بتلميحات.
* يتم تجاهل السمات `ClickHouseJsonPath` و`ClickHouseJsonIgnore` في String mode (إذ لا تعمل إلا في Binary mode).

***

<h4 id="type-map-writing-other">
  أنواع أخرى
</h4>

| نوع ClickHouse | أنواع .NET المقبولة | ملاحظات |
| - | - | - |
| UUID | `Guid`, `string` | تُحلَّل السلسلة النصية باعتبارها Guid |
| IPv4 | `IPAddress`, `string` | يجب أن يكون IPv4؛ وتُحلَّل السلسلة النصية باستخدام `IPAddress.Parse()` |
| IPv6 | `IPAddress`, `string` | يجب أن يكون IPv6؛ وتُحلَّل السلسلة النصية باستخدام `IPAddress.Parse()` |
| Nothing | أيّ نوع | لا يكتب شيئًا (no-op) |
| Dynamic | — | **غير مدعوم** (throws `NotImplementedException`) |
| Array(T) | `IList`, `null` | تؤدي القيمة null إلى كتابة مصفوفة فارغة. بالنسبة إلى الأنواع المتداخلة (`Array(Array(T))` وما هو أعمق)، يُقبل كلٌّ من الأشكال المتعرجة (`T[][]`, `List<List<T>>`) ومصفوفات CLR المستطيلة متعددة الأبعاد (`T[,]`, `T[,,]`, …)؛ ويجب أن تتطابق رتبة CLR مع عمق التداخل في ClickHouse. |
| Tuple(T1, T2, ...) | `ITuple`, `IList` | يجب أن يتطابق عدد العناصر مع رتبة Tuple. راجع [تحذير ValueTuple](#valuetuple-caveat) للعناصر التي يزيد عددها على 7. |
| Map(K, V) | `IDictionary`, `IEnumerable<KeyValuePair<K, V>>` | تُقبل سلسلة الأزواج (على سبيل المثال `List<KeyValuePair<K, V>>` الناتجة عن `MapReadMode=KeyValuePairs`) في كلا وضعي القراءة، ويمكن أن تكرّر المفتاح نفسه. ينطبق ذلك على عمليات الإدراج الثنائية وعلى معاملات الاستعلام |
| Nullable(T) | `null`, `DBNull`, أو الأنواع المقبولة لـ T | يكتب بايت علامة null قبل القيمة |
| Enum8 | `string`, `sbyte`, أنواع رقمية | يُبحث عن السلسلة النصية في قاموس enum |
| Enum16 | `string`, `short`, أنواع رقمية | يُبحث عن السلسلة النصية في قاموس enum |
| LowCardinality(T) | الأنواع المقبولة لـ T | يُفوَّض إلى النوع الأساسي |
| SimpleAggregateFunction | الأنواع المقبولة للنوع الأساسي | يُفوَّض إلى النوع الأساسي |
| Nested(...) | `IList` من tuples | يجب أن يتطابق عدد العناصر مع عدد الحقول |
| Variant(T1, T2, ...) | قيمة تطابق أحد T1 أو T2 أو ... | Throws `ArgumentException` إذا لم يطابق أي نوع |
| QBit(T, dim) | `IList` | يُفوَّض إلى Array؛ والبُعد بيانات وصفية فقط |

***

<h4 id="type-map-writing-geometry">
  أنواع Geometry
</h4>

| نوع ClickHouse | أنواع .NET المقبولة | ملاحظات |
| - | - | - |
| Point | `System.Drawing.Point`, `ITuple`, `IList` (عنصران) | |
| Ring | `IList` من عناصر Point | |
| LineString | `IList` من عناصر Point | |
| Polygon | `IList` من عناصر Ring | |
| MultiLineString | `IList` من عناصر LineString | |
| MultiPolygon | `IList` من عناصر Polygon | |
| Geometry | أي نوع من أنواع Geometry أعلاه | Variant لجميع أنواع Geometry |

***

<h4 id="type-map-writing-not-supported">
  غير مدعوم عند الكتابة
</h4>

| نوع ClickHouse | ملاحظات |
| - | - |
| Dynamic | يؤدي إلى إطلاق `NotImplementedException` |
| AggregateFunction | يؤدي إلى إطلاق `AggregateFunctionException` |

***

<h3 id="nested-type-handling">
  التعامل مع النوع Nested
</h3>

يمكن قراءة النوع Nested في ClickHouse (`Nested(...)`) وكتابته باستخدام دلالات المصفوفات.

```sql theme={null}
CREATE TABLE test.nested (
    id UInt32,
    params Nested (param_id UInt8, param_val String)
) ENGINE = Memory
```

```csharp theme={null}
var row1 = new object[] { 1, new[] { 1, 2, 3 }, new[] { "v1", "v2", "v3" } };
var row2 = new object[] { 2, new[] { 4, 5, 6 }, new[] { "v4", "v5", "v6" } };

await client.InsertBinaryAsync(
    "test.nested",
    new[] { "id", "params.param_id", "params.param_val" },
    new[] { row1, row2 }
);
```

<h2 id="logging-and-diagnostics">
  التسجيل والتشخيص
</h2>

يتكامل عميل ClickHouse لـ .NET مع تجريدات `Microsoft.Extensions.Logging` لتوفير تسجيل خفيف الوزن يُفعَّل عند الحاجة. وعند تمكينه، يُصدر برنامج التشغيل رسائل منظَّمة لأحداث دورة حياة الاتصال، وتنفيذ الأوامر، وعمليات النقل، وعمليات الإدراج المجمّع. ويظل التسجيل اختياريًا بالكامل—فالتطبيقات التي لا تُعدّ مُسجِّلًا تواصل العمل دون أي عبء إضافي.

<h3 id="logging-quick-start">
  البدء السريع
</h3>

```csharp theme={null}
using ClickHouse.Driver;
using Microsoft.Extensions.Logging;

var loggerFactory = LoggerFactory.Create(builder =>
{
    builder
        .AddConsole()
        .SetMinimumLevel(LogLevel.Information);
});

var settings = new ClickHouseClientSettings("Host=localhost;Port=8123")
{
    LoggerFactory = loggerFactory
};

using var client = new ClickHouseClient(settings);
```

<h4 id="logging-appsettings-config">
  استخدام appsettings.json
</h4>

يمكنك ضبط مستويات التسجيل باستخدام إعدادات .NET القياسية:

```csharp theme={null}
using ClickHouse.Driver;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;

var configuration = new ConfigurationBuilder()
    .SetBasePath(Directory.GetCurrentDirectory())
    .AddJsonFile("appsettings.json")
    .Build();

var loggerFactory = LoggerFactory.Create(builder =>
{
    builder
        .AddConfiguration(configuration.GetSection("Logging"))
        .AddConsole();
});

var settings = new ClickHouseClientSettings("Host=localhost;Port=8123")
{
    LoggerFactory = loggerFactory
};

using var client = new ClickHouseClient(settings);
```

<h4 id="logging-inmemory-config">
  استخدام تهيئة داخل الذاكرة
</h4>

يمكنك أيضًا ضبط مستوى تفصيل التسجيل لكل فئة في الشيفرة:

```csharp theme={null}
using ClickHouse.Driver;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;

var categoriesConfiguration = new Dictionary<string, string>
{
    { "LogLevel:Default", "Warning" },
    { "LogLevel:ClickHouse.Driver.Connection", "Information" },
    { "LogLevel:ClickHouse.Driver.Command", "Debug" }
};

var config = new ConfigurationBuilder()
    .AddInMemoryCollection(categoriesConfiguration)
    .Build();

using var loggerFactory = LoggerFactory.Create(builder =>
{
    builder
        .AddConfiguration(config)
        .AddSimpleConsole();
});

var settings = new ClickHouseClientSettings("Host=localhost;Port=8123")
{
    LoggerFactory = loggerFactory
};

using var client = new ClickHouseClient(settings);
```

<h3 id="logging-categories">
  الفئات والبواعث
</h3>

يستخدم برنامج التشغيل فئات مخصّصة بحيث يمكنك ضبط مستويات السجل بدقة لكل مكوّن:

| الفئة | المصدر | أبرز الجوانب |
| - | - | - |
| `ClickHouse.Driver.Connection` | `ClickHouseConnection` | دورة حياة الاتصال، واختيار HTTP client factory، وفتح الاتصال وإغلاقه، وإدارة الجلسات. |
| `ClickHouse.Driver.Command` | `ClickHouseCommand` | بدء تنفيذ الاستعلام واكتماله، والتوقيت، ومعرّفات الاستعلام، وإحصاءات الخادم، وتفاصيل الخطأ. |
| `ClickHouse.Driver.Transport` | `ClickHouseConnection` | طلبات HTTP streaming منخفضة المستوى، وعلامات الضغط، ورموز حالة الاستجابة، وإخفاقات النقل. |
| `ClickHouse.Driver.Client` | `ClickHouseClient` | عمليات insert الثنائية، والاستعلامات، والعمليات الأخرى |
| `ClickHouse.Driver.NetTrace` | `TraceHelper` | تتبّع الشبكة، فقط عند تمكين وضع التصحيح |

<h4 id="logging-config-example">
  مثال: تشخيص مشكلات الاتصال
</h4>

```json theme={null}
{
    "Logging": {
        "LogLevel": {
            "ClickHouse.Driver.Connection": "Trace",
            "ClickHouse.Driver.Transport": "Trace"
        }
    }
}
```

سيُسجِّل هذا ما يلي:

* اختيار HTTP client factory ‏(التجمّع default مقابل اتصال واحد)
* تهيئة HTTP handler ‏(SocketsHttpHandler أو HttpClientHandler)
* إعدادات connection pool ‏(MaxConnectionsPerServer وPooledConnectionLifetime وما إلى ذلك)
* إعدادات timeout ‏(ConnectTimeout وExpect100ContinueTimeout وما إلى ذلك)
* تهيئة SSL/TLS
* أحداث فتح الاتصال وإغلاقه
* تتبّع معرّف الجلسة

<h3 id="logging-debugmode">
  وضع Debug: تتبّع الشبكة والتشخيص
</h3>

للمساعدة في تشخيص مشكلات الشبكة، تتضمن مكتبة برنامج التشغيل أداة مساعدة تُمكّن التتبّع منخفض المستوى للمكوّنات الداخلية للشبكات في .NET. ولتمكينه، يجب تمرير LoggerFactory مع تعيين المستوى إلى Trace، وضبط EnableDebugMode على true (أو تمكينه يدويًا عبر الصنف `ClickHouse.Driver.Diagnostic.TraceHelper`). ستُسجَّل الأحداث ضمن الفئة `ClickHouse.Driver.NetTrace`. تحذير: سيؤدي ذلك إلى إنشاء سجلات شديدة التفصيل، وسيؤثر في الأداء. لا يُنصح بتمكين وضع Debug في بيئة الإنتاج.

```csharp theme={null}
var loggerFactory = LoggerFactory.Create(builder =>
{
    builder
        .AddConsole()
        .SetMinimumLevel(LogLevel.Trace); // Must be Trace level to see network events
});

var settings = new ClickHouseClientSettings()
{
    LoggerFactory = loggerFactory,
    EnableDebugMode = true,  // Enable low-level network tracing
};
```

<h2 id="opentelemetry">
  OpenTelemetry
</h2>

يوفّر برنامج التشغيل دعمًا مدمجًا للتتبّع الموزّع في OpenTelemetry عبر واجهة برمجة تطبيقات .NET [`System.Diagnostics.Activity`](https://learn.microsoft.com/en-us/dotnet/core/diagnostics/distributed-tracing). وعند تمكينه، يُنشئ برنامج التشغيل spans لعمليات قاعدة البيانات يمكن تصديرها إلى أنظمة observability الخلفية مثل Jaeger أو ClickHouse نفسه (عبر [OpenTelemetry Collector](/ar/guides/use-cases/observability/build-your-own/integrating-opentelemetry)).

<h3 id="opentelemetry-enabling">
  تمكين التتبّع
</h3>

في تطبيقات ASP.NET Core، أضِف `ActivitySource` الخاص ببرنامج تشغيل ClickHouse إلى تهيئة OpenTelemetry:

```csharp theme={null}
builder.Services.AddOpenTelemetry()
    .WithTracing(tracing => tracing
        .AddSource(ClickHouseDiagnosticsOptions.ActivitySourceName)  // Subscribe to ClickHouse driver spans
        .AddAspNetCoreInstrumentation()
        .AddOtlpExporter());             // Or AddJaegerExporter(), etc.
```

لتطبيقات سطر الأوامر، أو للاختبار، أو للإعداد اليدوي:

```csharp theme={null}
using OpenTelemetry;
using OpenTelemetry.Trace;

var tracerProvider = Sdk.CreateTracerProviderBuilder()
    .AddSource(ClickHouseDiagnosticsOptions.ActivitySourceName)
    .AddConsoleExporter()
    .Build();
```

<h3 id="opentelemetry-attributes">
  سمات span
</h3>

يتضمن كل span سمات قاعدة البيانات القياسية في OpenTelemetry، بالإضافة إلى إحصاءات query الخاصة بـ ClickHouse التي يمكن استخدامها في debugging.

| السمة | الوصف |
| - | - |
| `db.system` | تكون دائمًا `"clickhouse"` |
| `db.name` | اسم قاعدة البيانات |
| `db.user` | اسم المستخدم |
| `db.statement` | SQL query (إذا كان enabled) |
| `db.clickhouse.read_rows` | الصفوف التي قرأها الـ query |
| `db.clickhouse.read_bytes` | البايتات التي قرأها الـ query |
| `db.clickhouse.written_rows` | الصفوف التي كتبها الـ query |
| `db.clickhouse.written_bytes` | البايتات التي كتبها الـ query |
| `db.clickhouse.elapsed_ns` | execution time على جهة الخادم بالنانوثانية |

<h3 id="opentelemetry-configuration">
  خيارات التكوين
</h3>

تحكَّم في سلوك التتبّع باستخدام `ClickHouseDiagnosticsOptions`:

```csharp theme={null}
using ClickHouse.Driver.Diagnostic;

// Include SQL statements in spans (default: false for security)
ClickHouseDiagnosticsOptions.IncludeSqlInActivityTags = true;

// Truncate long SQL statements (default: 1000 characters)
ClickHouseDiagnosticsOptions.StatementMaxLength = 500;
```

<Warning>
  قد يؤدي تفعيل `IncludeSqlInActivityTags` إلى كشف بيانات حساسة ضمن التتبعات الخاصة بك. استخدمه بحذر في بيئات الإنتاج.
</Warning>

<h2 id="tls-configuration">
  إعدادات TLS
</h2>

عند الاتصال بـ ClickHouse عبر HTTPS، يمكنك تهيئة سلوك TLS/SSL بعدة طرق.

<h3 id="custom-certificate-validation">
  التحقق المخصص من الشهادات
</h3>

بالنسبة إلى بيئات الإنتاج التي تتطلب منطقًا مخصصًا للتحقق من الشهادات، وفّر `HttpClient` خاصًا بك مع معالج `ServerCertificateCustomValidationCallback` مُعدّ:

```csharp theme={null}
using System.Net;
using System.Net.Security;
using ClickHouse.Driver;

var handler = new HttpClientHandler
{
    // No AutomaticDecompression needed: the driver decodes compressed responses itself.
    ServerCertificateCustomValidationCallback = (message, cert, chain, sslPolicyErrors) =>
    {
        // Example: Accept a specific certificate thumbprint
        if (cert?.Thumbprint == "YOUR_EXPECTED_THUMBPRINT")
            return true;

        // Example: Accept certificates from a specific issuer
        if (cert?.Issuer.Contains("YourOrganization") == true)
            return true;

        // Default: Use standard validation
        return sslPolicyErrors == SslPolicyErrors.None;
    },
};

var httpClient = new HttpClient(handler) { Timeout = TimeSpan.FromMinutes(5) };

var settings = new ClickHouseClientSettings
{
    Host = "my.clickhouse.server",
    Protocol = "https",
    HttpClient = httpClient,
};

using var client = new ClickHouseClient(settings);
```

<Note>
  اعتبارات مهمة عند استخدام `HttpClient` مخصّص

  * **فك الضغط التلقائي**: اترك `AutomaticDecompression` معطّلًا. فالـ driver يفكّ ترميز الاستجابات المضغوطة بنفسه، لذا لا حاجة إليه — بل إن تمكينه ينقلب ضدّك من جهة الطلب: فعند الإرسال *يضيف* المعالج أيضًا كل خوارزمية في قناعه إلى ترويسة `Accept-Encoding` الصادرة، فيوسّع ما أعلن عنه الـ driver، ما يتيح لـ ClickHouse الردّ باستخدام codec لم تطلبه. راجع [فك ضغط الاستجابة](#response-decompression).
  * **مهلة الخمول**: اضبط `PooledConnectionIdleTimeout` على قيمة أقل من `keep_alive_timeout` الخاص بالخادم (10 ثوانٍ في ClickHouse Cloud) لتجنّب أخطاء الاتصال الناتجة عن الاتصالات شبه المفتوحة.
</Note>

<h2 id="performance-tuning">
  ضبط الأداء
</h2>

يوضّح هذا القسم كيفية استخدام الـ client لتحقيق الأداء الأمثل، والخيارات المختلفة التي يمكنك ضبطها لرفع كفاءة الـ client بما يلائم حالة الاستخدام الخاصة بك.

<h3 id="perf-at-a-glance">
  لمحة سريعة
</h3>

\| إذا كنت | افعل هذا |
\|---|---|---|
\| تقرأ الصفوف إلى كائنات POCO | استخدم [`QueryAsync<T>`](#perf-read-path) بدلاً من `MapTo<T>` |
\| تنفّذ عمليات إدراج كبيرة | ارفع قيمة [`InsertOptions.BatchSize`](#perf-insert-batching) |
\| تشغّل تطبيق طرفية أو worker كثيف الإدراج | فعّل [Server GC](#perf-gc) |
\| تقرأ نتائج كبيرة عبر الشبكة | أبقِ ضغط الاستجابة مفعّلاً (وهو الوضع الافتراضي) |
\| تُدرج عبر اتصال سريع | جرّب [`InsertOptions.Compressor = null`](#perf-compression) |
\| تُدرج في الجدول نفسه مرات عديدة | استخدم [`UseSchemaCache` أو `ColumnTypes`](#skip-schema-query) |
\| تقرأ نتائج ضخمة جداً | ارفع قيمة [`ReadBufferSize`](#perf-buffers) |

***

<h3 id="perf-read-path">
  القراءة: اختر مسار التجسيد
</h3>

هناك ثلاث طرق للحصول على صف من النتيجة، وتكلفتها ليست واحدة. فبعض المسارات تُغلِّف القيم في كائنات (boxing)، ما يزيد التخصيصات ويقلّل الأداء.

| كيفية القراءة | تغليف كل قيمة | ملاحظات |
| - | - | - |
| `QueryAsync<T>` | **لا** | تقرأ من الـ stream مباشرةً إلى خصائصك. المسار السريع. |
| مُوصِّلات القارئ ذات النوع المحدد (`GetInt32`، `GetInt64`، `GetDouble`، `GetGuid`، `GetDateTime`، `GetFieldValue<T>`) | **لا** | قراءة دون تغليف من مخزن قيم ذي نوع محدد. |
| `MapTo<T>` | نعم | يُجسِّد الصف أولًا، ثم ينسخ القيم منه. |
| `GetValue` و`GetValues` | نعم | تُعيد `object`، لذا لا بدّ من تغليف القيمة عند طلبها. |

لقراءة 1,000,000 صف من 105 أعمدة من مجموعة بيانات *hits*:

| واجهة برمجة التطبيقات | المُخصَّص |
| - | -: |
| `QueryAsync<T>` | **1,372 MB** |
| `MapTo<T>` | 3,133 MB |

```csharp theme={null}
// Fast path: register the type once, then stream rows directly into it.
client.RegisterPocoType<HitRow>();

await foreach (var row in client.QueryAsync<HitRow>("SELECT * FROM hits"))
    Process(row);
```

<Note>
  *تحصل أطر ORM على المسار السريع عند استخدامها accessors ذات أنواع محددة.* يسجّل linq2db الدوال `GetInt64`
  و`GetDouble` و`GetDateTime` لكل عمود، فتتم القراءة دون تغليف (boxing). أما الشيفرة التي تقرأ عبر
  `GetValue` (بما في ذلك نتيجة `dynamic` من Dapper) فتغلّف كل قيمة. وإذا كان استعلام ORM كثير التنفيذ
  ويقرأ عبر `GetValue`، فاستخدم `QueryAsync<T>` لهذا الاستعلام تحديدًا.
</Note>

***

<h3 id="perf-insert-batching">
  الإدراج: batch size والتوازي
</h3>

يُعدّ batch size أكبر عامل تحكّم منفرد في throughput الإدراج. القيمة الافتراضية لـ `InsertOptions.BatchSize` هي
100,000 row.

**استخدم batches كبيرة.** في عملية إدراج بحجم 1,000,000 row، أدّت زيادة حجم كل batch من 10,000 إلى 100,000 row
إلى النتائج التالية:

| Insert | 10,000 rows/batch | 100,000 rows/batch | |
| - | -: | -: | -: |
| POCO | 15,308 ms | 7,853 ms | −49% |
| `object[]` | 17,027 ms | 10,671 ms | −37% |

إذا تعذّر عليك التحكّم في batch size (مثلًا عندما يرسل عدد كبير من producers الصغيرة rows بشكل مستقل) فاستخدم [async inserts](#async-inserts) ودع الخادم يتولّى batching.

**الرفع المتوازي.** القيمة الافتراضية لـ `InsertOptions.MaxDegreeOfParallelism` هي `1`. زِدها لإرسال
batches بالتوازي. ويظهر أثر ذلك بوضوح أكبر عند تفعيل الضغط، لأن كل batch يُضغط عندئذٍ
على thread خاص به. ولا تعمل sessions مع الإدراج المتوازي: إما أن تُعطّل sessions، أو تُبقي
`MaxDegreeOfParallelism = 1`.

**أزل schema probe.** يرسل كل استدعاء لـ `InsertBinaryAsync` أولًا query بالشكل `SELECT ... WHERE 1=0`
لتحديد column types. راجع [تخطّي schema probe query](#skip-schema-query) للتخلّص من هذه
الرحلة الذهابية والإيابية باستخدام `ColumnTypes` أو `UseSchemaCache`.

<Note>
  ينطبق مسار الإدراج الخالي من التغليف (box-free) على format الافتراضي `RowBinary`. أما `RowBinaryWithDefaults` فيتعيّن عليه
  فحص كل value للعثور على وسم `DBDefault`، لذا يظل على المسار الأبطأ.
</Note>

***

<h3 id="perf-compression">
  الضغط: الاتجاهان لا يتفقان
</h3>

يقايض الضغط وحدة المعالجة المركزية بالبايتات. وتتوقف جدوى هذه المقايضة على اتجاه النقل، وعرض النطاق الترددي لاتصالك بخادم ClickHouse، وكيفية تفاعل بياناتك مع خوارزمية الضغط التي اخترتها، وما إذا كنت تدفع مقابل كل بايت يُنقل.

**عمليات القراءة:** أبقِ الضغط مفعّلًا، إلا إذا كان الخادم يعمل على الجهاز نفسه. وهذا هو الإعداد الافتراضي. وبالمقارنة مع عدم استخدام الضغط، أعطى `zstd` عند المستوى 1 النتائج التالية:

| من العميل إلى الخادم | أثر الضغط |
| - | - |
| المضيف نفسه (loopback) | يكلّف 8% |
| منطقة السحابة نفسها | **يوفّر 16%** |
| منطقة واحدة بعيدًا | **يوفّر 33%** |

**عمليات الإدراج:** قِس قبل أن تضغط. فقد لا تكون الوفورات كافية لتبرير تفعيله. وضع في اعتبارك أيضًا أن فك الضغط يضيف حملًا إضافيًا على الخادم؛ وهو حمل متواضع مع Zstd وLZ4 لكنه قد يكون مرتفعًا مع خوارزميات أخرى (مثل Brotli).

لإيقاف ضغط عمليات الإدراج:

```csharp theme={null}
var options = new InsertOptions { Compressor = null };
await client.InsertBinaryAsync("my_table", columns, rows, options);
```

لاختيار الـ codec ومستويات الضغط وكيفية تحديد نقطة التقاطع المناسبة لحالتك، راجع
[ضبط الضغط](#tuning-compression).

***

<h3 id="perf-buffers">
  المخازن المؤقتة
</h3>

يحدّد `ReadBufferSize` حجم المخزن المؤقت الذي يقرأ استجابات HTTP، وقيمته الافتراضية 64 كيبيبايت.

يستعير المشغّل (driver) هذا المخزن المؤقت من تجمّع مشترك ويعيده إليه عند التخلّص من القارئ، لذا لا يجري تخصيص ذاكرة جديد مع كل استعلام. زِد هذه القيمة لتقليل عدد مرات إعادة ملء المخزن المؤقت مع النتائج الكبيرة. ويحتفظ المشغّل بمخزن مؤقت واحد لكل قارئ مفتوح في الوقت نفسه، لذا يرتفع استهلاك الذاكرة كلما زاد حجم المخزن المؤقت وزاد عدد القرّاء المتزامنين.

```csharp theme={null}
var settings = new ClickHouseClientSettings("Host=localhost") { ReadBufferSize = 256 * 1024 };
```

<Warning>
  *تخلّص دائمًا من القارئ (reader).* فعند التخلص منه، يعيد القارئ الـ buffer الخاص به إلى الـ pool ويحرّر اتصال HTTP.
  أما ترك القارئ دون تخلص فلا يعيد الـ buffer إلى الـ pool وقد يُبقي اتصال HTTP
  غير متاح؛ كما أن عملية garbage collection الاعتيادية ليست بديلًا عن التخلص الصريح.
</Warning>

***

<h3 id="perf-gc">
  بيئة التشغيل وجامع المهملات (GC)
</h3>

**فعّل Server GC في التطبيقات كثيفة الإدراج.** فمع الشيفرة نفسها، والعدد نفسه من البايتات المخصصة، كان Workstation GC أبطأ بنسبة تصل إلى 97% في عمليات الإدراج مقارنةً بـ Server GC.

```xml theme={null}
<PropertyGroup>
  <ServerGarbageCollection>true</ServerGarbageCollection>
</PropertyGroup>
```

تضبط مشاريع ASP.NET Core هذا الإعداد مسبقًا، أما تطبيقات وحدة التحكم وخدمات الـ worker ومعظم صور الحاويات فلا تضبطه.

والسبب هو حجم ميزانية الجيل 0؛ إذ يستخدم Workstation GC ميزانية صغيرة، ولذلك لا تنتهي حياة الـ buffers قصيرة العمر التي ينشئها الـ insert في الجيل 0، بل تنتقل إلى الجيل 1، ما يزيد من الترقية ويؤدي إلى عمل أكبر بكثير على الجيل 2. ففي إحدى حالات الـ insert، بلغ عدد عمليات جمع الجيل 2 لكل 1,000 عملية 4,000 مع Server GC مقابل 73,000 مع Workstation GC.

<Note>
  إن Server GC إعداد يخص الـ throughput لا الـ latency. ففي القياسات نفسها، أمضى Server GC أقل من نصف إجمالي الوقت في حالة توقف مؤقت، لكن فترات التوقف المفردة لديه كانت أطول (المئين الخامس والتسعون 114.6 مللي ثانية مقابل 61.9 مللي ثانية). فإذا كانت خدمتك حساسة تجاه الـ tail latency، فقِس كلا الوضعين قبل الاختيار.
</Note>

***

<h3 id="perf-latency">
  زمن الاستجابة: أعد استخدام الاتصالات
</h3>

يستغرق إنشاء اتصال TCP جديد وتنفيذ مصافحة TLS وقتًا طويلًا نسبيًا.
وإعادة استخدام الاتصالات تقلّل بشكل ملحوظ من زمن استجابة استعلاماتك.

* لا تُنشئ عميلًا لكل طلب، فكل عميل جديد له `HttpClient` خاص به يُنشئ تجمّع اتصالات جديدًا،
  ويتكبّد كلفة المصافحة من جديد. استخدم `ClickHouseClient` واحدًا طوال عمر التطبيق؛ فهو آمن للاستخدام
  من خيوط متعددة ومصمّم للاستخدام كنسخة مفردة (singleton).
* في ADO.NET وأطر ORM، استخدم `ClickHouseDataSource` بحيث تتشارك جميع الاتصالات تجمّعًا واحدًا.

للاطلاع على المجموعة الكاملة من الأنماط، راجع
[عمر الاتصال وتجميع الاتصالات](#best-practices-connection-lifetime).

***

<h3 id="perf-measuring">
  قِس بنفسك
</h3>

في كثير من الحالات، يعتمد الأداء على بنية بياناتك، وسرعة اتصالك بالخادم،
وما إذا كنت تريد المفاضلة بين استهلاك CPU في العميل واستهلاكه في الخادم (أو العكس)، وقيود عتادك، وما إلى ذلك.
لذلك يُنصح بأن تقيس الأداء بنفسك استنادًا إلى بياناتك وبيئتك.

لمعرفة الجزء الذي ينجزه الخادم من العمل، عيّن `QueryOptions.QueryId` ثم اقرأ العدادات:

```sql theme={null}
SELECT ProfileEvents['UserTimeMicroseconds'] + ProfileEvents['SystemTimeMicroseconds'] AS cpu_us,
       ProfileEvents['NetworkSendBytes'] AS sent_bytes,
       query_duration_ms
FROM system.query_log
WHERE query_id = 'your-query-id' AND type = 'QueryFinish';
```

***

<h2 id="orm-support">
  دعم ORM
</h2>

تتطلب أطر ORM واجهة ADO.NET (`ClickHouseConnection`). ولإدارة دورة حياة الاتصالات على النحو الصحيح، أنشئ الاتصالات باستخدام `ClickHouseDataSource`:

```csharp theme={null}
// Register DataSource as singleton
var dataSource = new ClickHouseDataSource("Host=localhost;Username=default");

// Create connections for ORM use
await using var connection = await dataSource.OpenConnectionAsync();
// Pass connection to your ORM...
```

<h3 id="orm-support-dapper">
  Dapper
</h3>

يعمل `ClickHouse.Driver` مع Dapper. ويحوّل برنامج التشغيل تلقائيًا صياغة `@parameter` الخاصة بـ Dapper إلى الصياغة الأصلية في ClickHouse `{parameter:Type}`، مع استنتاج الأنواع من قيم .NET.

استخدم `ClickHouseDataSource` لإدارة مدة بقاء الاتصال على نحو صحيح:

```csharp theme={null}
var dataSource = new ClickHouseDataSource("Host=localhost");
services.AddSingleton(dataSource); // Register as singleton in DI

using var connection = dataSource.CreateConnection();
```

<h4 id="dapper-parameter-passing">
  أنماط تمرير المَعلمات
</h4>

جميع أنماط مَعلمات Dapper القياسية مدعومة:

**الكائنات المجهولة:**

```csharp theme={null}
await connection.ExecuteAsync(
    "INSERT INTO users (id, name, balance) VALUES (@Id, @Name, @Balance)",
    new { Id = 1, Name = "alice", Balance = 3.14 });
```

**فئات POCO:**

```csharp theme={null}
class InsertParams
{
    public int Id { get; set; }
    public string Name { get; set; }
    public double Balance { get; set; }
}

var param = new InsertParams { Id = 42, Name = "bob", Balance = 99.9 };
await connection.ExecuteAsync(
    "INSERT INTO users (id, name, balance) VALUES (@Id, @Name, @Balance)", param);
```

**القاموس:**

```csharp theme={null}
var parameters = new Dictionary<string, object> { { "Id", 2 } };
var rows = await connection.QueryAsync<User>(
    "SELECT id, name FROM users WHERE id = @Id", parameters);
```

**`DynamicParameters` (من قاموس أو كائن مجهول الاسم):**

```csharp theme={null}
var dynParams = new DynamicParameters(new { Id = 1 });
// or: new DynamicParameters(new Dictionary<string, object> { { "Id", 1 } });

var rows = await connection.QueryAsync<User>(
    "SELECT id, name FROM users WHERE id = @Id", dynParams);
```

<h4 id="dapper-pocos">
  الاستعلام عن POCOs
</h4>

يربط Dapper الأعمدة بالخصائص حسب الاسم (من دون حساسية لحالة الأحرف):

```csharp theme={null}
class User
{
    public int Id { get; set; }
    public string Name { get; set; }
    public double Balance { get; set; }
}

// From a table
var users = (await connection.QueryAsync<User>("SELECT id, name, balance FROM users")).ToList();

// From a literal
var row = (await connection.QueryAsync<User>("SELECT 1 as id, 'hello' as name, 2.5 as balance")).Single();
```

<h4 id="dapper-clickhouse-param-syntax">
  صياغة المعلمات الأصلية في ClickHouse
</h4>

عندما تحتاج إلى تحكم صريح في النوع، استخدم مباشرةً في SQL صياغة `{param:Type}` الخاصة بـ ClickHouse مع `Dictionary<string, object>` لقيم المعلمات. لا تجمع بين صياغة `@param` وصياغة `{param:Type}` للمعلمة نفسها.

```csharp theme={null}
var parameters = new Dictionary<string, object> { { "value", 42 } };
var result = await connection.QueryAsync<int>("SELECT {value:Int32}", parameters);
```

<h4 id="dapper-where-in">
  WHERE IN
</h4>

**تعمل ميزة توسيع IN المدمجة في Dapper:**

```csharp theme={null}
var rows = await connection.QueryAsync<User>(
    "SELECT id, name FROM users WHERE id IN @Ids ORDER BY id",
    new { Ids = new[] { 1, 3, 5 } });
```

يعيد Dapper صياغة ذلك إلى `WHERE id IN (@Ids1, @Ids2, @Ids3)`، ويحوّل برنامج التشغيل كل معلمة موسَّعة.

**كما تعمل الدالة `has()` في ClickHouse أيضًا مع معلمة من النوع Array:**

```csharp theme={null}
var parameters = new Dictionary<string, object> { { "ids", new[] { 1, 3, 5 } } };
var rows = await connection.QueryAsync<User>(
    "SELECT id, name FROM users WHERE has({ids:Array(Int32)}, id) ORDER BY id",
    parameters);
```

<h4 id="dapper-type-handlers">
  معالِجات الأنواع المخصّصة
</h4>

تتطلّب بعض أنواع ClickHouse، مثل `ITuple` و`BigInteger` و`ClickHouseDecimal`، تسجيل معالِجات عند بدء التشغيل:

```csharp theme={null}
// ClickHouseDecimal (for Decimal64/128/256 columns)
SqlMapper.AddTypeHandler(new ClickHouseDecimalHandler());

// BigInteger (for Int128/Int256/UInt128/UInt256 columns)
SqlMapper.AddTypeHandler(new BigIntegerHandler());

// IPAddress (for IPv4/IPv6 columns)
SqlMapper.AddTypeHandler(new IpAddressHandler());
```

راجع [مثال Dapper](https://github.com/ClickHouse/clickhouse-cs/blob/main/examples/ORM/ORM_001_Dapper.cs) للاطلاع على مثال لتنفيذ معالج أنواع.

<h4 id="dapper-contrib">
  Dapper.Contrib
</h4>

يعمل كلٌّ من `GetAll<T>()` و`Get<T>(id)`. أما `Insert<T>()` فلا يعمل، إذ يولّد صياغة SQL Server (`SCOPE_IDENTITY`, `[]`). ويُوصى باستخدام الطريقة الأصلية `InsertBinaryAsync` في `ClickHouseClient` بدلًا من ذلك.

```csharp theme={null}
[Table("test.users")]
record class UserRecord(int Id, string Name, DateTime Timestamp);

var all = await connection.GetAllAsync<UserRecord>();
var one = await connection.GetAsync<UserRecord>(1);
```

يجب أن تتطابق أسماء الخصائص تمامًا مع أسماء أعمدة ClickHouse (تراعي حالة الأحرف).

<h4 id="dapper-limitations">
  القيود
</h4>

| العنصر | الحالة | التفاصيل |
| - | - | - |
| Tuple كـ **نتيجة** | يعمل | يتطلب تسجيل `SqlMapper.TypeHandler<ITuple>` |
| Tuple كـ **معلمة** | غير مدعوم | لا يستطيع Dapper إجراء تسلسل لـ `ITuple`/`Tuple<>` كقيمة `DbParameter` |
| أنواع Nested كمعلمة | غير مدعوم | للسبب نفسه — يرفض Dapper الأنواع المعقدة كقيم للمعلمات |
| أنواع Geo كمعلمة | غير مدعوم | Point, Ring, Polygon, LineString, MultiLineString, MultiPolygon |
| `Dapper.Contrib.Insert<T>()` | غير مدعوم | يُنشئ صياغة خاصة بـ SQL Server |
| النوع `Nothing` | غير مدعوم | لا يوجد له تمثيل ذو معنى في .NET |

<h3 id="orm-support-linq2db">
  Linq2db
</h3>

يتوافق برنامج التشغيل هذا مع [linq2db](https://github.com/linq2db/linq2db)، وهو ORM خفيف الوزن وموفّر LINQ لـ .NET. راجع موقع المشروع للاطلاع على وثائق تفصيلية.

**مثال على الاستخدام:**

أنشئ `DataConnection` باستخدام موفّر ClickHouse:

```csharp theme={null}
using LinqToDB;
using LinqToDB.Data;
using LinqToDB.DataProvider.ClickHouse;

var connectionString = "Host=localhost;Port=8123;Database=default";
var options = new DataOptions()
    .UseClickHouse(connectionString, ClickHouseProvider.ClickHouseDriver);

await using var db = new DataConnection(options);
```

يمكن تحديد تعيينات الجداول باستخدام السمات أو التهيئة بأسلوب Fluent. إذا كانت أسماء الفئة والخاصية لديك تطابق تمامًا أسماء الجدول والعمود، فلا حاجة إلى أي تهيئة:

```csharp theme={null}
public class Product
{
    public int Id { get; set; }
    public string Name { get; set; }
    public decimal Price { get; set; }
}
```

**الاستعلام عن:**

```csharp theme={null}
await using var db = new DataConnection(options);

var products = await db.GetTable<Product>()
    .Where(p => p.Price > 100)
    .OrderByDescending(p => p.Name)
    .ToListAsync();
```

**النسخ المجمّع:**

استخدم `BulkCopyAsync` لإجراء عمليات إدراج مجمّعة بكفاءة.

```csharp theme={null}
await using var db = new DataConnection(options);
var table = db.GetTable<Product>();

var options = new BulkCopyOptions
{
    MaxBatchSize = 100000,
    MaxDegreeOfParallelism = 1,
    WithoutSession = true
};

await table.BulkCopyAsync(options, products);
```

<h3 id="orm-support-ef-core">
  Entity Framework Core
</h3>

موفّر Entity Framework Core الرسمي لـ ClickHouse. اربط فئات C# بجداول ClickHouse، ونفّذ الاستعلامات باستخدام LINQ، وأدرِج البيانات عبر `SaveChanges` — وكل ذلك باستخدام أنماط EF Core المألوفة.

* **NuGet**: [`ClickHouse.EntityFrameworkCore`](https://www.nuget.org/packages/ClickHouse.EntityFrameworkCore)
* **المصدر**: [GitHub](https://github.com/ClickHouse/ClickHouse.EntityFrameworkCore)

<Note>
  هذا الموفّر قيد التطوير النشط. يدعم الإصدار الحالي استعلامات LINQ (بما في ذلك عمليات JOIN، والاستعلامات الفرعية، وعمليات المجموعات)، و`INSERT` عبر `SaveChanges` / `BulkInsertAsync`، وعمليات الترحيل مع دعم DDL الكامل (CREATE / ALTER / DROP)، وتهيئة محرك الجدول الخاصة بـ ClickHouse. لا يدعم `UPDATE` / `DELETE`.
</Note>

<h4 id="ef-core-installation">
  التثبيت
</h4>

```bash theme={null}
dotnet add package ClickHouse.EntityFrameworkCore
```

يتطلب .NET 10.0 وEF Core 10.

<h4 id="ef-core-quick-start">
  البدء السريع
</h4>

عرّف الكيان و`DbContext`، ثم أجرِ استعلامًا باستخدام LINQ:

```csharp theme={null}
using Microsoft.EntityFrameworkCore;

public class PageView
{
    public long Id { get; set; }
    public string Path { get; set; }
    public DateOnly Date { get; set; }
    public string UserAgent { get; set; }
}

public class AnalyticsContext : DbContext
{
    public DbSet<PageView> PageViews { get; set; }

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        => optionsBuilder.UseClickHouse("Host=localhost;Database=analytics");
}

// Query
await using var ctx = new AnalyticsContext();

var topPages = await ctx.PageViews
    .Where(v => v.Date >= new DateOnly(2024, 1, 1))
    .GroupBy(v => v.Path)
    .Select(g => new { Path = g.Key, Views = g.Count() })
    .OrderByDescending(x => x.Views)
    .Take(10)
    .ToListAsync();
```

<h4 id="ef-core-types">
  الأنواع المدعومة
</h4>

| الفئة | أنواع ClickHouse | أنواع CLR |
| - | - | - |
| **الأعداد الصحيحة** | `Int8`–`Int64`, `UInt8`–`UInt64` | `sbyte`, `short`, `int`, `long`, `byte`, `ushort`, `uint`, `ulong` |
| **الأعداد الصحيحة الكبيرة** | `Int128`, `Int256`, `UInt128`, `UInt256` | `BigInteger` |
| **الأعداد ذات الفاصلة العائمة** | `Float32`, `Float64`, `BFloat16` | `float`, `double` |
| **الأعداد العشرية** | `Decimal(P,S)`, `Decimal32(S)`, `Decimal64(S)`, `Decimal128(S)` | `decimal` أو `ClickHouseDecimal` |
| **Bool** | `Bool` | `bool` |
| **السلاسل النصية** | `String`, `FixedString(N)` | `string` |
| **التعدادات** | `Enum8(...)`, `Enum16(...)` | `string` أو `enum` في C# |
| **التاريخ/الوقت** | `Date`, `Date32`, `DateTime`, `DateTime64(P, 'TZ')` | `DateOnly`, `DateTime` |
| **الوقت** | `Time`, `Time64(N)` | `TimeSpan` |
| **UUID** | `UUID` | `Guid` |
| **الشبكة** | `IPv4`, `IPv6` | `IPAddress` |
| **المصفوفات** | `Array(T)` | `T[]`, `List<T>`, `IList<T>`, `ICollection<T>`, `IReadOnlyList<T>`, `IReadOnlyCollection<T>`, `IEnumerable<T>` |
| **الخرائط** | `Map(K, V)` | `Dictionary<K,V>` |
| **Tuples** | `Tuple(T1, ...)` | `Tuple<...>` أو `ValueTuple<...>` |
| **Variant** | `Variant(T1, T2, ...)` | `object` |
| **Dynamic** | `Dynamic` | `object` |
| **JSON** | `Json` | `JsonNode` أو `string` |
| **الأنواع الجغرافية** | `Point`, `Ring`, `LineString`, `Polygon`, `MultiLineString`, `MultiPolygon`, `Geometry` | `Tuple<double,double>` ومصفوفات منها؛ و`object` لـ `Geometry` |
| **المغلِّفات** | `Nullable(T)`, `LowCardinality(T)` | تُفك تلقائيًا |

استخدم `ClickHouseDecimal` (من `ClickHouse.Driver.Numerics`) بدلًا من `decimal` عندما تحتاج إلى الدقة الكاملة لأعمدة `Decimal128`/`Decimal256`، لأن `decimal` في .NET يقتصر على 28–29 رقمًا معنويًا.

<h4 id="ef-core-linq">
  عمليات LINQ المدعومة
</h4>

**الاستعلامات:** `Where`, `OrderBy`, `Take`, `Skip`, `Select`, `First`, `Single`, `Any`, `All`, `Count`, `Distinct`, `AsNoTracking`

**GROUP BY والتجميع:** `GroupBy` مع `Count`, `LongCount`, `Sum`, `Average`, `Min`, `Max` — بما في ذلك `HAVING` (استخدام `.Where()` بعد `.GroupBy()`)، وإجراء عدة عمليات تجميع ضمن إسقاط واحد، واستخدام `OrderBy` على نتائج التجميع.

**عمليات JOIN:** `Join` (INNER)، وأنماط `GroupJoin`/`SelectMany`‏ (LEFT وCROSS). تُرجِع LEFT JOIN قيمة `null` فعلية للصفوف غير المتطابقة (راجع [دلالات null في LEFT JOIN](#ef-core-join-nulls) أدناه).

**الاستعلامات الفرعية:** `Contains` / `IN` المترابطة، و`Any` / `EXISTS`، و`All`، والاستعلامات الفرعية scalar ضمن الإسقاطات.

**عمليات المجموعات:** `Concat` (→ `UNION ALL`)، و`Union` (→ `UNION DISTINCT`)، و`Intersect`، و`Except`.

**المجموعات المحلية المضمنة:** تُترجَم عمليات join و`Contains` على المجموعات الموجودة في الذاكرة (`int[]`, `List<T>`, إلخ) إلى سلسلة من عمليات UNION.

**طرق String:** `Contains`, `StartsWith`, `EndsWith`, `IndexOf`, `Replace`, `Substring`, `Trim`/`TrimStart`/`TrimEnd`, `ToLower`, `ToUpper`, `Length`, `IsNullOrEmpty`, `Concat` (والمعامل `+`).

**الدوال الرياضية:** تُترجَم طرق `Math` و`MathF` القياسية إلى ما يقابلها في ClickHouse — بما يشمل الدوال الحسابية واللوغاريتمية والمثلثية ودوال الأدوات المساعدة.

<h5 id="ef-core-join-nulls">
  دلالات NULL في LEFT JOIN
</h5>

يحقن الموفّر `set_join_use_nulls=1` تلقائيًا في كل مسار اتصال لمواءمة توقّعات Entity Framework بشأن سلوك JOIN.

إذا كان خادم ClickHouse أو ملف التعريف لديك يمنع تغيير هذا الإعداد (على سبيل المثال، ملف تعريف بقيمة `readonly=1`)، فأوقِف هذا السلوك باستخدام:

```csharp theme={null}
optionsBuilder.UseClickHouse(connectionString, o => o.DisableJoinNullSemantics());
```

عند تفعيل خيار إلغاء الاشتراك، يعيد LEFT JOIN القيم الافتراضية لأعمدة ClickHouse، ولا يعود اكتشاف التنقل في EF المعتمد على `null` يعمل كما هو متوقع. استخدم مقارنات صريحة مع `0` / `""` بدلًا من `== null`.

<h4 id="ef-core-insert">
  إدراج البيانات
</h4>

يستخدم `SaveChanges` واجهة برمجة تطبيقات `InsertBinaryAsync` الأصلية لبرنامج التشغيل — بترميز RowBinary مع جسم طلب مضغوط، وهو أكثر كفاءة بكثير من عبارات SQL ذات المعلمات:

```csharp theme={null}
await using var ctx = new AnalyticsContext();

ctx.PageViews.Add(new PageView
{
    Id = 1,
    Path = "/home",
    Date = new DateOnly(2024, 6, 15),
    UserAgent = "Mozilla/5.0"
});

await ctx.SaveChangesAsync();
```

تنتقل الكيانات من `Added` إلى `Unchanged` بعد الحفظ، تمامًا كما يحدث مع أي موفّر آخر لـ EF Core.

يمكن ضبط **حجم الدفعة** (الافتراضي 1000):

```csharp theme={null}
optionsBuilder.UseClickHouse("Host=localhost", o => o.MaxBatchSize(5000));
```

<h4 id="ef-core-bulk-insert">
  الإدراج المجمّع
</h4>

لعمليات التحميل عالية الإنتاجية، استخدم `BulkInsertAsync` بدلًا من `SaveChanges`. هذه طريقة امتداد لـ `DbContext` تتجاوز بالكامل متتبّع التغييرات في EF Core، وآلية حلّ الهوية، وإدارة الحالة — إذ تستدعي مباشرةً `InsertBinaryAsync` الخاصة ببرنامج التشغيل باستخدام ترميز RowBinary وجسم طلب مضغوط.

وهذا يجعلها مناسبة لتحميل مجموعات بيانات كبيرة عندما لا تحتاج إلى تتبّع الكيانات بعد الإدراج:

```csharp theme={null}
var events = Enumerable.Range(0, 100_000)
    .Select(i => new PageView
    {
        Id = i,
        Path = $"/page/{i}",
        Date = DateOnly.FromDateTime(DateTime.Today)
    });

long rowsInserted = await ctx.BulkInsertAsync(events);
```

يمكن أن يكون الإدخال أي `IEnumerable<T>` — إذ يمر عبر الكيانات دون تحميلها جميعًا إلى الذاكرة. قيمة الإرجاع هي عدد الصفوف المُدرجة. لا تُرفَق الكيانات بـ `DbContext` بعد الإدراج، لذلك لا يحدث انتقال في الحالة من `Added` إلى `Unchanged`.

<h4 id="ef-core-enums">
  التعدادات
</h4>

يمكن تعيين أعمدة `Enum8`/`Enum16` في ClickHouse كخصائص `string` أو كأنواع `enum` في C#. وعند استخدام تعدادات C#، يُجري الموفّر التحويل تلقائيًا بين التعداد وتمثيله النصي:

```csharp theme={null}
public enum Status { Active, Inactive, Pending }

public class User
{
    public long Id { get; set; }
    public Status Status { get; set; }
}

// Query with enum values
var active = await ctx.Users
    .Where(u => u.Status == Status.Active)
    .ToListAsync();
```

<h4 id="ef-core-value-converters">
  تحويلات الأنواع المخصّصة
</h4>

يتيح لك نظام `ValueConverter` في EF Core تعيين الأنواع المخصّصة إلى أنواع يدعمها الموفّر بالفعل. ولا يرى الموفّر نوعك المخصّص مطلقًا، إذ يتولّى EF Core التحويل عند نقطة الفصل.

**التحويل على مستوى كل خاصية:**

```csharp theme={null}
public class Money
{
    public decimal Amount { get; set; }
    public string Currency { get; set; }
}

public class Order
{
    public long Id { get; set; }
    public Money Price { get; set; }
}

// In OnModelCreating:
modelBuilder.Entity<Order>()
    .Property(o => o.Price)
    .HasConversion(
        m => $"{m.Amount}|{m.Currency}",
        s => new Money
        {
            Amount = decimal.Parse(s.Split('|')[0]),
            Currency = s.Split('|')[1]
        })
    .HasColumnType("String");
```

**فئة مُحوِّل قابلة لإعادة الاستخدام:**

```csharp theme={null}
public class MoneyConverter : ValueConverter<Money, string>
{
    public MoneyConverter() : base(
        m => $"{m.Amount}|{m.Currency}",
        s => Parse(s)) { }

    private static Money Parse(string s)
    {
        var parts = s.Split('|');
        return new Money { Amount = decimal.Parse(parts[0]), Currency = parts[1] };
    }
}

// Apply to a single property:
.HasConversion<MoneyConverter>()

// Or apply to all properties of a type via conventions:
protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder)
{
    configurationBuilder.Properties<Money>()
        .HaveConversion<MoneyConverter>();
}
```

<h4 id="ef-core-column-types">
  تعليقات توضيحية لنوع العمود
</h4>

بالنسبة إلى الأنواع القياسية مثل `string` و`int` و`DateTime` وغيرها، يستنتج الموفّر نوع ClickHouse تلقائيًا. أمّا الأنواع ذات المعلمات والأغلفة، فيلزم تحديد نوع ClickHouse صراحةً.

**استخدام التعليقات التوضيحية للبيانات (السمات):**

```csharp theme={null}
using System.ComponentModel.DataAnnotations.Schema;
using Microsoft.EntityFrameworkCore;

[Table("sensor_readings")]
public class SensorReading
{
    public long Id { get; set; }

    [Column(TypeName = "Array(String)")]
    public string[] Tags { get; set; }

    [Column(TypeName = "Map(String, String)")]
    public Dictionary<string, string> Metadata { get; set; }

    [Column(TypeName = "Nullable(Float64)")]
    public double? Value { get; set; }

    [Column(TypeName = "Decimal128(18)")]
    public decimal HighPrecision { get; set; }
}
```

**استخدام أسلوب fluent API في `OnModelCreating`:**

```csharp theme={null}
modelBuilder.Entity<SensorReading>(e =>
{
    e.ToTable("sensor_readings");
    e.Property(x => x.Tags).HasColumnType("Array(String)");
    e.Property(x => x.Metadata).HasColumnType("Map(String, String)");
    e.Property(x => x.Value).HasColumnType("Nullable(Float64)");
    e.Property(x => x.Category).HasColumnType("LowCardinality(String)");
    e.Property(x => x.HighPrecision).HasColumnType("Decimal128(18)");
});
```

الأغلفة المتداخلة مثل `Array(Nullable(Int32))` و`LowCardinality(Nullable(String))` مدعومة — إذ يفكّ الموفّر تغليف `Nullable` و`LowCardinality` تلقائيًا عند كل مستوى من مستويات التداخل.

<h4 id="ef-core-variant-dynamic">
  أعمدة Variant وDynamic
</h4>

تُقابِل أعمدة ClickHouse `Variant(T1, T2, ...)` و`Dynamic` النوع `object` في .NET. وبما أن `object` عام جدًا بحيث لا يتيح استنتاج النوع تلقائيًا، يجب التصريح بنوع التخزين صراحةً باستخدام `.HasColumnType()`:

```csharp theme={null}
public class Event
{
    public long Id { get; set; }
    public object? Payload { get; set; }
}

// In OnModelCreating:
entity.Property(e => e.Payload).HasColumnType("Variant(String, UInt64, Array(UInt64))");
// or:
entity.Property(e => e.Payload).HasColumnType("Dynamic");
```

عند القراءة، يُفك تسلسل القيمة تلقائيًا إلى نوع .NET الموافق للمميِّز المخزَّن (مثل `string` و`ulong` و`ulong[]`).

<h4 id="ef-core-json">
  أعمدة JSON
</h4>

يدعم المزوّد نوع العمود `Json` في ClickHouse، ويعيّنه إلى `System.Text.Json.Nodes.JsonNode` (بشكل أساسي) أو `string` (عبر `ValueConverter` تلقائي):

```csharp theme={null}
using System.Text.Json.Nodes;

public class Event
{
    public long Id { get; set; }
    public JsonNode? Data { get; set; }
}

// In OnModelCreating:
entity.Property(e => e.Data).HasColumnType("Json");
```

تتم قراءة JSON وكتابته من خلال كلٍّ من `SaveChanges` و`BulkInsertAsync`:

```csharp theme={null}
ctx.Events.Add(new Event
{
    Id = 1,
    Data = JsonNode.Parse("""{"action": "click", "x": 100, "y": 200}""")
});
await ctx.SaveChangesAsync();

var ev = await ctx.Events.Where(e => e.Id == 1).SingleAsync();
string action = ev.Data!["action"]!.GetValue<string>(); // "click"
```

إذا كنت تفضّل سلاسل JSON الخام، فاضبط الخاصية لتكون `string` مع نوع عمود `Json` — يطبّق الموفّر `ValueConverter` تلقائيًا:

```csharp theme={null}
public class Event
{
    public long Id { get; set; }
    public string? Data { get; set; }  // raw JSON string
}

entity.Property(e => e.Data).HasColumnType("Json");
```

<Note>
  * **عدم ترجمة JSON path** — لا يُترجَم `entity.Data["name"]` في LINQ إلى صياغة SQL `data.name` الخاصة بـ ClickHouse. طبّق عامل التصفية على الأعمدة غير JSON وافحص JSON في الذاكرة.
  * **دلالات NULL** — يعيد JSON type في ClickHouse القيمة `{}` (كائنًا فارغًا) لقيم NULL بدلًا من SQL NULL.
  * **دقة الأعداد الصحيحة** — يخزّن JSON في ClickHouse جميع الأعداد الصحيحة بصيغة `Int64`. عند القراءة عبر `JsonNode`، استخدم `GetValue<long>()` بدلًا من `GetValue<int>()`.
</Note>

<h4 id="ef-core-engines">
  محركات الجداول
</h4>

اضبط محركات جداول ClickHouse والعبارات الخاصة بكل محرك عبر واجهة `ToTable(name, t => ...)` بأسلوب الاستدعاءات المتسلسلة. عند عدم ضبط أي محرك، يستخدم الموفّر `MergeTree` افتراضيًا، مع استمداد `ORDER BY` من المفتاح الأساسي للكيان.

```csharp theme={null}
modelBuilder.Entity<Event>(e =>
{
    e.ToTable("events", t => t
        .HasMergeTreeEngine()
        .WithOrderBy("UserId", "Timestamp")
        .WithPartitionBy("toYYYYMM(Timestamp)")
        .WithPrimaryKey("UserId")
        .WithSettings("index_granularity = 8192"));
});
```

عائلات المحركات المدعومة:

| المحرّك | الأسلوب المتسلسل | ملاحظات |
| - | - | - |
| `MergeTree` | `HasMergeTreeEngine()` | الافتراضي عند عدم تكوين أي شيء |
| `ReplacingMergeTree` | `HasReplacingMergeTreeEngine("Version", "IsDeleted")` or `HasReplacingMergeTreeEngine<T>(e => e.Version)` | عمودا Version وIsDeleted اختياريان |
| `SummingMergeTree` | `HasSummingMergeTreeEngine(…)` or `HasSummingMergeTreeEngine<T>(e => new { … })` | أعمدة الجمع اختيارية |
| `AggregatingMergeTree` | `HasAggregatingMergeTreeEngine()` | — |
| `CollapsingMergeTree` | `HasCollapsingMergeTreeEngine("Sign")` or `HasCollapsingMergeTreeEngine<T>(e => e.Sign)` | يجب أن يكون العمود `Sign` من النوع `Int8` |
| `VersionedCollapsingMergeTree` | `HasVersionedCollapsingMergeTreeEngine("Sign", "Version")` or `<T>(e => e.Sign, e => e.Version)` | — |
| `GraphiteMergeTree` | `HasGraphiteMergeTreeEngine("config_section")` | — |
| `Log`, `TinyLog`, `StripeLog`, `Memory` | `HasLogEngine()`, `HasTinyLogEngine()`, `HasStripeLogEngine()`, `HasMemoryEngine()` | بدون ORDER BY / PARTITION BY |

**بنود المحرّك:** `WithOrderBy`, `WithPartitionBy`, `WithPrimaryKey`, `WithSampleBy`, `WithTtl`, `WithSettings`. ترتبط جميعها بمنشئ المحرّك المُعاد من `HasXxxEngine()`.

**ميزات على مستوى العمود:** `HasCodec`, `HasTtl`, `HasComment`, `HasDefault` — جميعها تدخل في عمليات الترحيل.

**فهارس تخطي البيانات** — عبر `HasIndex(...).HasSkippingIndexType(...)`:

```csharp theme={null}
modelBuilder.Entity<Event>()
    .HasIndex(e => e.UserId)
    .HasSkippingIndexType("minmax")
    .HasGranularity(4);

// Index with parameters (e.g. bloom_filter, tokenbf_v1):
modelBuilder.Entity<Event>()
    .HasIndex(e => e.Tag)
    .HasSkippingIndexType("bloom_filter")
    .HasSkippingIndexParams("0.01")
    .HasGranularity(1);
```

يُتجاهَل الفهرس القياسي (غير القائم على تخطي البيانات) بصمت، إذ لا يوجد في ClickHouse ما يقابله. أما الفهارس الفريدة فتؤدي إلى ظهور استثناء، لأن ClickHouse لا يفرض التفرد.

<h4 id="ef-core-migrations">
  عمليات الترحيل
</h4>

سير العمل القياسي لعمليات الترحيل في EF Core:

```bash theme={null}
dotnet ef migrations add InitialCreate
dotnet ef database update
```

العمليات المدعومة:

| العملية | الناتج |
| - | - |
| `CREATE TABLE` | يتضمن عبارة engine، وORDER BY، وPARTITION BY، وSETTINGS، ومرمّزات الأعمدة/TTL/التعليقات/القيم الافتراضية |
| `ALTER TABLE ADD COLUMN` | — |
| `ALTER TABLE DROP COLUMN` | — |
| `ALTER TABLE MODIFY COLUMN` | يعالج تغيير النوع، بالإضافة إلى إضافة/إزالة السمات (CODEC, TTL, COMMENT, DEFAULT) |
| `ALTER TABLE RENAME COLUMN` | — |
| `RENAME TABLE` | — |
| `ALTER TABLE ADD INDEX` / `DROP INDEX` | فهارس تخطي البيانات فقط |
| `CREATE DATABASE` / `DROP DATABASE` | عبر `EnsureCreated` / `EnsureDeleted` وعمليات الترحيل |

<h4 id="ef-core-limitations">
  قيود الترحيل
</h4>

| الميزة | السبب |
| - | - |
| المفاتيح الخارجية | لا يفرض ClickHouse المفاتيح الخارجية. ترفض عمليات الترحيل `AddForeignKey`، ويُصدر مدقق النموذج تحذيرًا عند بناء النموذج. |
| القيود الفريدة / الفهارس الفريدة | لا يفرض ClickHouse التفرد. وتؤدي الفهارس الفريدة إلى إطلاق استثناء أثناء الترحيل. |
| القيم التي ينشئها الخادم (الزيادة التلقائية / `IDENTITY`) | لا يوجد ما يقابلها في ClickHouse. |
| أعمدة `Nested(…)` | غير مدعومة بعد كنوع CLR مُعيَّن. |
| الكيانات المملوكة بصيغة JSON (`.ToJson()`) | لم يُنفَّذ بعد التعيين البنيوي لـ JSON للكيانات المملوكة. استخدم `JsonNode` / `string` في عمود `Json` بدلًا من ذلك (راجع [أعمدة JSON](#ef-core-json)). |

وبالإضافة إلى عمليات الترحيل، لا يزال الموفّر لا يدعم أيضًا ما يلي:

* **`UPDATE` / `DELETE`**
* **المعاملات**: `BeginTransaction` عملية no-op. لا يوجد دعم لمعاملات ACID في ClickHouse.
* **ترجمة استعلامات JSON path**: ‏`entity.Data["key"]` في LINQ لا يُترجم إلى صياغة SQL الخاصة بـ ClickHouse، أي `data.key`. استخدم التصفية على أعمدة غير JSON وافحص JSON في الذاكرة.

<h2 id="limitations">
  القيود
</h2>

<h3 id="valuetuple-caveat">
  Tuples التي تضم 8 عناصر أو أكثر وبها Tuple متداخلة في الموضع الأخير
</h3>

تستخدم أنواع C# `ValueTuple` التي تضم أكثر من 7 عناصر نمط تداخل يُنشئه المصرّف: إذ تكون الوسيطة العامة الثامنة (`TRest`) هي نفسها `ValueTuple` تحتوي على العناصر المتبقية. على سبيل المثال، يُصرَّف `(int, int, int, int, int, int, int, string, string)` إلى `ValueTuple<int, int, int, int, int, int, int, ValueTuple<string, string>>`.

يؤدي ذلك إلى التباس عندما يكون عمود ClickHouse عبارة عن Tuple من 8 عناصر ويكون العنصر الأخير فيه هو نفسه Tuple — مثل `Tuple(Int32, Int32, Int32, Int32, Int32, Int32, Int32, Tuple(String, String))`. لا يستطيع برنامج التشغيل التمييز بين:

* **Tuple مسطحة من 9 عناصر** (تداخل TRest الذي يُنشئه المصرّف)
* **Tuple من 8 عناصر** يكون عنصرها الأخير `Tuple(String, String)` متداخلة

كلتاهما تنتجان نوع .NET نفسه: `ValueTuple<int, int, int, int, int, int, int, ValueTuple<string, string>>`.

يتعامل برنامج التشغيل مع الوسيطة الثامنة على أنها TRest (أي يُسطّحها)، ما يعني أن حالة Tuple ذات 8 عناصر مع Tuple متداخلة ستُسلسَل بصورة غير صحيحة.

ينطبق ذلك على كلٍّ من `System.Tuple` و`ValueTuple` لأن كليهما يستخدم تداخل TRest عند وجود أكثر من 7 عناصر. أما Tuples التي تضم 7 عناصر أو أقل، أو التي لا يكون عنصرها الأخير نفسه Tuple، فلا تتأثر.

**الحل البديل:** لفّ Tuple الداخلية بطبقة إضافية حتى يتمكن برنامج التشغيل من تمييزها عن تداخل TRest:

```csharp theme={null}
// Instead of this (ambiguous — is it 8 elements or 9 flat?):
Tuple.Create(1, 2, 3, 4, 5, 6, 7, Tuple.Create("a", "b"))

// Do this (unambiguous — inner tuple is wrapped):
Tuple.Create(1, 2, 3, 4, 5, 6, 7, Tuple.Create(Tuple.Create("a", "b")))
```

***

<h3 id="aggregatefunction-columns">
  أعمدة AggregateFunction
</h3>

لا يمكن الاستعلام عن الأعمدة من النوع `AggregateFunction(...)` أو الإدراج فيها مباشرةً.

للإدراج:

```sql theme={null}
INSERT INTO t VALUES (uniqState(1));
```

للاختيار:

```sql theme={null}
SELECT uniqMerge(c) FROM t;
```

***
