diff --git a/SQL-Server-SQLCLR/Projects/ClickHouseClient/Apps/ClickHouseClient.CLI/App.config b/SQL-Server-SQLCLR/Projects/ClickHouseClient/Apps/ClickHouseClient.CLI/App.config
new file mode 100644
index 0000000..c414bbb
--- /dev/null
+++ b/SQL-Server-SQLCLR/Projects/ClickHouseClient/Apps/ClickHouseClient.CLI/App.config
@@ -0,0 +1,50 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/SQL-Server-SQLCLR/Projects/ClickHouseClient/Apps/ClickHouseClient.CLI/ClickHouseClient.CLI.csproj b/SQL-Server-SQLCLR/Projects/ClickHouseClient/Apps/ClickHouseClient.CLI/ClickHouseClient.CLI.csproj
new file mode 100644
index 0000000..187712c
--- /dev/null
+++ b/SQL-Server-SQLCLR/Projects/ClickHouseClient/Apps/ClickHouseClient.CLI/ClickHouseClient.CLI.csproj
@@ -0,0 +1,61 @@
+
+
+
+
+ Debug
+ AnyCPU
+ {D3D02050-BD31-460D-8087-76D98A226F78}
+ Exe
+ YPermitin.SQLCLR.ClickHouseClient.CLI
+ ClickHouseClient.CLI
+ v4.8
+ 512
+ true
+ true
+
+ 9.0
+
+
+ AnyCPU
+ true
+ full
+ false
+ bin\Debug\
+ DEBUG;TRACE
+ prompt
+ 4
+
+
+ AnyCPU
+ pdbonly
+ true
+ bin\Release\
+ TRACE
+ prompt
+ 4
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {ac5f49bf-4526-47a3-8034-75f84108cee5}
+ ClickHouseClient.Entry
+
+
+
+
\ No newline at end of file
diff --git a/SQL-Server-SQLCLR/Projects/ClickHouseClient/Apps/ClickHouseClient.CLI/Program.cs b/SQL-Server-SQLCLR/Projects/ClickHouseClient/Apps/ClickHouseClient.CLI/Program.cs
new file mode 100644
index 0000000..ee7dfac
--- /dev/null
+++ b/SQL-Server-SQLCLR/Projects/ClickHouseClient/Apps/ClickHouseClient.CLI/Program.cs
@@ -0,0 +1,231 @@
+using System;
+using System.Data;
+using System.Data.SqlClient;
+using YPermitin.SQLCLR.ClickHouseClient.Entry;
+using YPermitin.SQLCLR.ClickHouseClient.Entry.Extensions;
+using YPermitin.SQLCLR.ClickHouseClient.Models;
+
+namespace ClickHouseClient.CLI
+{
+ internal class Program
+ {
+ static void Main(string[] args)
+ {
+ Console.WriteLine("Начало проверки работы с ClickHouse.");
+
+ // Строка подключения к SQL Server
+ EntryBase.ConnectionString = @"server=localhost;database=master;trusted_connection=true;";
+ // Строка подключения к ClickHouse
+ string clickHouseConnectionString = @"Host=yy-comp;Port=8123;Username=default;password=;Database=default;";
+
+ Console.WriteLine("Строка подключения SQL Server: {0}", EntryBase.ConnectionString);
+ Console.WriteLine("Строка подключения ClickHouse: {0}", clickHouseConnectionString);
+
+ Console.Write("Установка соединения с SQL Server...");
+ // Создаем соединение с SQL Server для дальнейшей работы
+ using (SqlConnection sqlConnection = new SqlConnection(EntryBase.ConnectionString))
+ {
+ sqlConnection.Open();
+ Console.WriteLine("OK!");
+ // Устанавливаем подключение для отладки
+ EntryBase.DebugConnection = sqlConnection;
+
+ #region ExecuteScalar
+
+ Console.WriteLine("Начало работы метода ExecuteScalar.");
+
+ // Выполняем запрос с возвратом одного значения
+ string clickHouseVersion = EntryClickHouseClient.ExecuteScalar(
+ connectionString: clickHouseConnectionString.ToSqlChars(),
+ queryText: "SELECT version()".ToSqlChars())
+ .ToStringFromSqlChars();
+ Console.WriteLine("Версия ClickHouse: {0}", clickHouseVersion);
+
+ Console.WriteLine("Окончание работы метода ExecuteScalar.");
+
+ #endregion
+
+ #region ExecuteStatement
+
+ Console.WriteLine("Начало работы метода ExecuteStatement.");
+
+ // Выполняем запрос без возврата результата.
+ // В качестве примера создаем таблицу, предварительно удалив, если она существовала.
+
+ Console.WriteLine("Удаляем существующую таблицу, если она существует.");
+ EntryClickHouseClient.ExecuteStatement(
+ connectionString: clickHouseConnectionString.ToSqlChars(),
+ queryText: @"
+DROP TABLE IF EXISTS SimpleTable
+".ToSqlChars());
+
+ Console.WriteLine("Создаем таблицу.");
+ EntryClickHouseClient.ExecuteStatement(
+ connectionString: clickHouseConnectionString.ToSqlChars(),
+ queryText: @"
+CREATE TABLE IF NOT EXISTS SimpleTable
+(
+ Id UInt64,
+ Period datetime DEFAULT now(),
+ Name String
+)
+ENGINE = MergeTree
+ORDER BY Id;
+".ToSqlChars());
+
+ // А затем вставляем 100 записей
+ Console.WriteLine("Добавленые новых записей....");
+ for (int i = 1; i <= 10; i++)
+ {
+ string rowName = "Row " + i;
+ EntryClickHouseClient.ExecuteStatement(
+ connectionString: clickHouseConnectionString.ToSqlChars(),
+ queryText: (@"
+INSERT INTO SimpleTable
+(
+ Id,
+ Name
+)
+VALUES(" + i + @", '" + rowName + @"');
+").ToSqlChars()
+ );
+
+ Console.WriteLine("Добавлена запись {0} - {1}", i, rowName);
+ }
+
+ Console.WriteLine("Окончание работы метода ExecuteStatement.");
+
+ #endregion
+
+ #region ExecuteSimple
+
+ Console.WriteLine("Начало работы метода ExecuteSimple.");
+
+ // Выполняем просто запрос с возвратом результата.
+ // У этого метода одно ограничение - возвращается только первая колонока запроса SELECT
+ // и только в виде строки.
+ // Для возвращения нескольких колонок можно возвращать кортеж, который будет преобразован к JSON-строке.
+
+ var simpleQueryResult = EntryClickHouseClient.ExecuteSimple(
+ connectionString: clickHouseConnectionString.ToSqlChars(),
+ queryText: @"
+SELECT
+ tuple(name, engine, data_path)
+FROM `system`.`databases`
+".ToSqlChars());
+
+ var enumerator = simpleQueryResult.GetEnumerator();
+ int rowsCount = 0;
+ while (enumerator.MoveNext())
+ {
+ Console.WriteLine(((ExecuteSimpleRowResult)enumerator.Current).ResultValue);
+ rowsCount++;
+ }
+
+ Console.WriteLine("Количество записей из результата запроса: {0}.", rowsCount);
+
+ Console.WriteLine("Окончание работы метода ExecuteSimple.");
+
+ #endregion
+
+ #region ExecuteToTempTable
+
+ Console.WriteLine("Начало работы метода ExecuteToTempTable.");
+
+ // Создаем временную таблицу для сохранения результата запроса из ClickHouse
+ Console.WriteLine("Создаем временную таблицу.");
+ string sqlCreateTempTable = @"
+IF(OBJECT_ID('tempdb..#logs') IS NOT NULL)
+ DROP TABLE #logs;
+CREATE TABLE #logs
+(
+ [EventTime] datetime2(0),
+ [Query] nvarchar(max),
+ [Tables] nvarchar(max),
+ [QueryId] uniqueidentifier
+);
+";
+ SqlCommand sqlCreateTempTableCommand = new SqlCommand(sqlCreateTempTable, sqlConnection);
+ sqlCreateTempTableCommand.CommandType = System.Data.CommandType.Text;
+ sqlCreateTempTableCommand.ExecuteNonQuery();
+
+ // Выполняем запрос к ClickHouse и сохраняем во временную таблицу
+ Console.WriteLine("Выполняем запрос к ClickHouse и сохраняем результат во временную таблицу.");
+ EntryClickHouseClient.ExecuteToTempTable(
+ connectionString: clickHouseConnectionString.ToSqlChars(),
+ queryText: @"
+select
+ event_time,
+ query,
+ tables,
+ query_id
+from `system`.query_log
+limit 1000
+".ToSqlChars(),
+ tempTableName: "#logs".ToSqlChars());
+
+ int totalRows = 0;
+ SqlCommand sqlTempTableRows = new SqlCommand("SELECT COUNT(*) FROM #logs", sqlConnection);
+ sqlTempTableRows.CommandType = System.Data.CommandType.Text;
+ totalRows = (int)sqlTempTableRows.ExecuteScalar();
+ Console.WriteLine("Всего записей прочитано: {0}", totalRows);
+
+ Console.WriteLine("Окончание работы метода ExecuteToTempTable.");
+
+ #endregion
+
+ #region ExecuteToGlobalTempTable
+
+ Console.WriteLine("Начало работы метода ExecuteToGlobalTempTable.");
+
+ // Создаем временную таблицу для сохранения результата запроса из ClickHouse
+ Console.WriteLine("Создаем ГЛОБАЛЬНУЮ временную таблицу.");
+ string sqlCreateGlobalTempTable = @"
+IF(OBJECT_ID('tempdb..##logs') IS NOT NULL)
+ DROP TABLE #logs;
+CREATE TABLE ##logs
+(
+ [EventTime] datetime2(0),
+ [Query] nvarchar(max),
+ [Tables] nvarchar(max),
+ [QueryId] uniqueidentifier
+);
+";
+ SqlCommand sqlCreateGlobalTempTableCommand = new SqlCommand(sqlCreateGlobalTempTable, sqlConnection);
+ sqlCreateTempTableCommand.CommandType = System.Data.CommandType.Text;
+ sqlCreateTempTableCommand.ExecuteNonQuery();
+
+ // Выполняем запрос к ClickHouse и сохраняем во временную таблицу
+ Console.WriteLine("Выполняем запрос к ClickHouse и сохраняем результат во временную таблицу.");
+ EntryClickHouseClient.ExecuteToGlobalTempTable(
+ connectionString: clickHouseConnectionString.ToSqlChars(),
+ queryText: @"
+select
+ event_time,
+ query,
+ tables,
+ query_id
+from `system`.query_log
+limit 10
+".ToSqlChars(),
+ tempTableName: "##logs".ToSqlChars(),
+ sqlServerConnectionString: EntryBase.ConnectionString.ToSqlChars());
+
+ int totalRowsGlobal = 0;
+ SqlCommand sqlTempTableRowsGlobal = new SqlCommand("SELECT COUNT(*) FROM ##logs", sqlConnection);
+ sqlTempTableRowsGlobal.CommandType = System.Data.CommandType.Text;
+ totalRowsGlobal = (int)sqlTempTableRowsGlobal.ExecuteScalar();
+ Console.WriteLine("Всего записей прочитано: {0}", totalRowsGlobal);
+
+ Console.WriteLine("Окончание работы метода ExecuteToGlobalTempTable.");
+
+ #endregion
+ }
+
+ // Очищаем отладочное соединение SQL Server
+ EntryBase.DebugConnection = null;
+
+ Console.WriteLine("Окончание проверки работы с ClickHouse.");
+ }
+ }
+}
diff --git a/SQL-Server-SQLCLR/Projects/ClickHouseClient/Apps/ClickHouseClient.CLI/Properties/AssemblyInfo.cs b/SQL-Server-SQLCLR/Projects/ClickHouseClient/Apps/ClickHouseClient.CLI/Properties/AssemblyInfo.cs
new file mode 100644
index 0000000..7fd5b57
--- /dev/null
+++ b/SQL-Server-SQLCLR/Projects/ClickHouseClient/Apps/ClickHouseClient.CLI/Properties/AssemblyInfo.cs
@@ -0,0 +1,33 @@
+using System.Reflection;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+
+// Общие сведения об этой сборке предоставляются следующим набором
+// набора атрибутов. Измените значения этих атрибутов для изменения сведений,
+// связанные с этой сборкой.
+[assembly: AssemblyTitle("ClickHouseClient.CLI")]
+[assembly: AssemblyDescription("")]
+[assembly: AssemblyConfiguration("")]
+[assembly: AssemblyCompany("")]
+[assembly: AssemblyProduct("ClickHouseClient.CLI")]
+[assembly: AssemblyCopyright("Copyright © 2024")]
+[assembly: AssemblyTrademark("")]
+[assembly: AssemblyCulture("")]
+
+// Установка значения False для параметра ComVisible делает типы в этой сборке невидимыми
+// для компонентов COM. Если необходимо обратиться к типу в этой сборке через
+// из модели COM задайте для атрибута ComVisible этого типа значение true.
+[assembly: ComVisible(false)]
+
+// Следующий GUID представляет идентификатор typelib, если этот проект доступен из модели COM
+[assembly: Guid("d3d02050-bd31-460d-8087-76d98a226f78")]
+
+// Сведения о версии сборки состоят из указанных ниже четырех значений:
+//
+// Основной номер версии
+// Дополнительный номер версии
+// Номер сборки
+// Номер редакции
+//
+[assembly: AssemblyVersion("1.0.0.0")]
+[assembly: AssemblyFileVersion("1.0.0.0")]
diff --git a/SQL-Server-SQLCLR/Projects/ClickHouseClient/ClickHouseClient.sln b/SQL-Server-SQLCLR/Projects/ClickHouseClient/ClickHouseClient.sln
new file mode 100644
index 0000000..ff1b929
--- /dev/null
+++ b/SQL-Server-SQLCLR/Projects/ClickHouseClient/ClickHouseClient.sln
@@ -0,0 +1,50 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 17
+VisualStudioVersion = 17.12.35527.113
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Apps", "Apps", "{7C642548-3BED-4C89-8070-1DAFFE063F48}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Libs", "Libs", "{8A38C4D0-173F-4C72-84E8-BCC94AED1551}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Docs", "Docs", "{0324CCD0-9711-4209-84E7-3B28062B2328}"
+ ProjectSection(SolutionItems) = preProject
+ Readme.md = Readme.md
+ EndProjectSection
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{0E777D3D-82EF-4E67-9EF0-7804AEBDC3C6}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ClickHouseClient.CLI", "Apps\ClickHouseClient.CLI\ClickHouseClient.CLI.csproj", "{D3D02050-BD31-460D-8087-76D98A226F78}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ClickHouseClient", "Libs\ClickHouseClient\ClickHouseClient.csproj", "{DE8E7D56-8A86-4142-84A7-5CEB3162329F}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ClickHouseClient.Entry", "Libs\ClickHouseClient.Entry\ClickHouseClient.Entry.csproj", "{AC5F49BF-4526-47A3-8034-75F84108CEE5}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {D3D02050-BD31-460D-8087-76D98A226F78}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {D3D02050-BD31-460D-8087-76D98A226F78}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {D3D02050-BD31-460D-8087-76D98A226F78}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {D3D02050-BD31-460D-8087-76D98A226F78}.Release|Any CPU.Build.0 = Release|Any CPU
+ {DE8E7D56-8A86-4142-84A7-5CEB3162329F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {DE8E7D56-8A86-4142-84A7-5CEB3162329F}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {DE8E7D56-8A86-4142-84A7-5CEB3162329F}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {DE8E7D56-8A86-4142-84A7-5CEB3162329F}.Release|Any CPU.Build.0 = Release|Any CPU
+ {AC5F49BF-4526-47A3-8034-75F84108CEE5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {AC5F49BF-4526-47A3-8034-75F84108CEE5}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {AC5F49BF-4526-47A3-8034-75F84108CEE5}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {AC5F49BF-4526-47A3-8034-75F84108CEE5}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(NestedProjects) = preSolution
+ {D3D02050-BD31-460D-8087-76D98A226F78} = {7C642548-3BED-4C89-8070-1DAFFE063F48}
+ {DE8E7D56-8A86-4142-84A7-5CEB3162329F} = {8A38C4D0-173F-4C72-84E8-BCC94AED1551}
+ {AC5F49BF-4526-47A3-8034-75F84108CEE5} = {8A38C4D0-173F-4C72-84E8-BCC94AED1551}
+ EndGlobalSection
+EndGlobal
diff --git a/SQL-Server-SQLCLR/Projects/ClickHouseClient/Libs/ClickHouseClient.Entry/ClickHouseClient.Entry.csproj b/SQL-Server-SQLCLR/Projects/ClickHouseClient/Libs/ClickHouseClient.Entry/ClickHouseClient.Entry.csproj
new file mode 100644
index 0000000..7105e4d
--- /dev/null
+++ b/SQL-Server-SQLCLR/Projects/ClickHouseClient/Libs/ClickHouseClient.Entry/ClickHouseClient.Entry.csproj
@@ -0,0 +1,65 @@
+
+
+
+
+ Debug
+ AnyCPU
+ {AC5F49BF-4526-47A3-8034-75F84108CEE5}
+ Library
+ Properties
+ YPermitin.SQLCLR.ClickHouseClient.Entry
+ ClickHouseClient.Entry
+ v4.8
+ 512
+ true
+ 9.0
+
+
+
+ true
+ full
+ false
+ bin\Debug\
+ DEBUG;TRACE
+ prompt
+ 4
+
+
+ pdbonly
+ true
+ bin\Release\
+ TRACE
+ prompt
+ 4
+
+
+
+ ..\..\packages\Newtonsoft.Json.8.0.3\lib\net45\Newtonsoft.Json.dll
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {de8e7d56-8a86-4142-84a7-5ceb3162329f}
+ ClickHouseClient
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/SQL-Server-SQLCLR/Projects/ClickHouseClient/Libs/ClickHouseClient.Entry/EntryBase.cs b/SQL-Server-SQLCLR/Projects/ClickHouseClient/Libs/ClickHouseClient.Entry/EntryBase.cs
new file mode 100644
index 0000000..2eee5da
--- /dev/null
+++ b/SQL-Server-SQLCLR/Projects/ClickHouseClient/Libs/ClickHouseClient.Entry/EntryBase.cs
@@ -0,0 +1,23 @@
+using System.Data.SqlClient;
+
+namespace YPermitin.SQLCLR.ClickHouseClient.Entry
+{
+ public abstract class EntryBase
+ {
+ ///
+ /// Строка подключения к SQL Server.
+ ///
+ /// По умолчанию используется контекстное соединение,
+ /// из под которого выполнен вызов функции или процедуры со стороны SQL Server.
+ ///
+ public static string ConnectionString { get; set; }
+ = "context connection=true";
+
+ ///
+ /// Соединение SQL Server для целей отладки.
+ ///
+ /// При использовании расширения непосредственно на SQL Server не используется.
+ ///
+ public static SqlConnection DebugConnection { get; set; }
+ }
+}
diff --git a/SQL-Server-SQLCLR/Projects/ClickHouseClient/Libs/ClickHouseClient.Entry/EntryClickHouseClient.cs b/SQL-Server-SQLCLR/Projects/ClickHouseClient/Libs/ClickHouseClient.Entry/EntryClickHouseClient.cs
new file mode 100644
index 0000000..56f366d
--- /dev/null
+++ b/SQL-Server-SQLCLR/Projects/ClickHouseClient/Libs/ClickHouseClient.Entry/EntryClickHouseClient.cs
@@ -0,0 +1,662 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Data;
+using System.Data.Common;
+using System.Data.SqlClient;
+using System.Data.SqlTypes;
+using System.Net;
+using System.Security.Principal;
+using System.Text;
+using System.Text.RegularExpressions;
+using Microsoft.SqlServer.Server;
+using Newtonsoft.Json;
+using YPermitin.SQLCLR.ClickHouseClient.ADO;
+using YPermitin.SQLCLR.ClickHouseClient.Copy;
+using YPermitin.SQLCLR.ClickHouseClient.Models;
+using YPermitin.SQLCLR.ClickHouseClient.Utility;
+
+namespace YPermitin.SQLCLR.ClickHouseClient.Entry
+{
+ public class EntryClickHouseClient : EntryBase
+ {
+ private static SqlChars _emptyString = new SqlChars(string.Empty);
+ private static readonly Dictionary> TypeConverters = new Dictionary>();
+ static EntryClickHouseClient()
+ {
+ // Дата и время
+ TypeConverters.Add(typeof(DateTime), (sourceName, sourceType) =>
+ {
+ return "datetime2(0)";
+ });
+ // Строковой тип
+ TypeConverters.Add(typeof(string), (sourceName, sourceType) =>
+ {
+ return "nvarchar(max)";
+ });
+ // Дробное число
+ TypeConverters.Add(typeof(double), (sourceName, sourceType) =>
+ {
+ return "numeric(35,5)";
+ });
+ // Дробное число
+ TypeConverters.Add(typeof(float), (sourceName, sourceType) =>
+ {
+ return "numeric(35,5)";
+ });
+ // Целое число
+ TypeConverters.Add(typeof(Int16), (sourceName, sourceType) =>
+ {
+ return "numeric(25,0)";
+ });
+ TypeConverters.Add(typeof(Int32), (sourceName, sourceType) =>
+ {
+ return "numeric(25,0)";
+ });
+ TypeConverters.Add(typeof(UInt16), (sourceName, sourceType) =>
+ {
+ return "numeric(25,0)";
+ });
+ TypeConverters.Add(typeof(UInt32), (sourceName, sourceType) =>
+ {
+ return "numeric(25,0)";
+ });
+ // Целое число (большое)
+ TypeConverters.Add(typeof(Int64), (sourceName, sourceType) =>
+ {
+ return "numeric(25,0)";
+ });
+ TypeConverters.Add(typeof(UInt64), (sourceName, sourceType) =>
+ {
+ return "numeric(25,0)";
+ });
+ // Байт
+ TypeConverters.Add(typeof(byte), (sourceName, sourceType) =>
+ {
+ return "numeric(15,0)";
+ });
+ // Булево
+ TypeConverters.Add(typeof(bool), (sourceName, sourceType) =>
+ {
+ return "bit";
+ });
+ // IP-адрес
+ TypeConverters.Add(typeof(IPAddress), (sourceName, sourceType) =>
+ {
+ return "nvarchar(max)";
+ });
+ // Массивы
+ TypeConverters.Add(typeof(Array), (sourceName, sourceType) =>
+ {
+ return "nvarchar(max)";
+ });
+ // Словари
+ TypeConverters.Add(typeof(DictionaryBase), (sourceName, sourceType) =>
+ {
+ return "nvarchar(max)";
+ });
+ // Кортежи
+ TypeConverters.Add(typeof(Tuple), (sourceName, sourceType) =>
+ {
+ return "nvarchar(max)";
+ });
+ // Уникальный идентификатор
+ TypeConverters.Add(typeof(Guid), (sourceName, sourceType) =>
+ {
+ return "uniqueidentifier";
+ });
+ }
+
+ ///
+ /// Функция для выполнения запроса к ClickHouse и получения скалярного значения
+ ///
+ /// Строка подключения к ClickHouse
+ /// SQL-текст запроса для выполнения
+ /// Результат запроса, представленный строкой
+ [SqlFunction(DataAccess = DataAccessKind.Read)]
+ public static SqlChars ExecuteScalar(SqlChars connectionString, SqlChars queryText)
+ {
+ string connectionStringValue = new string(connectionString.Value);
+ string queryTextValue = new string(queryText.Value);
+
+ string resultAsString = string.Empty;
+
+ using (var connection = new ClickHouseConnection(connectionStringValue))
+ {
+ var queryResult = connection.ExecuteScalarAsync(queryTextValue)
+ .GetAwaiter().GetResult();
+
+ resultAsString = queryResult.ToString();
+ }
+
+ return new SqlChars(resultAsString);
+ }
+
+
+ ///
+ /// Функция для выполнения простого запроса.
+ ///
+ /// Возвращается только первая колонка из результата запроса в виде строки.
+ ///
+ /// Строка подключения к ClickHouse
+ /// SQL-текст запроса для выполнения
+ /// Набор результата запроса (только первая колонка в виде строки)
+ [SqlFunction(
+ FillRowMethodName = "ExecuteSimpleFillRow",
+ SystemDataAccess = SystemDataAccessKind.Read,
+ DataAccess = DataAccessKind.Read)]
+ public static IEnumerable ExecuteSimple(SqlChars connectionString, SqlChars queryText)
+ {
+ List resultRows = new List();
+
+ string connectionStringValue = new string(connectionString.Value);
+ string queryTextValue = new string(queryText.Value);
+
+ using (var connection = new ClickHouseConnection(connectionStringValue))
+ {
+ using (var reader = connection.ExecuteReaderAsync(queryTextValue)
+ .GetAwaiter().GetResult())
+ {
+ while(reader.Read())
+ {
+ resultRows.Add(new ExecuteSimpleRowResult()
+ {
+ ResultValue = ConvertTypeToSqlCommandType(reader.GetValue(0)).ToString()
+ });
+ }
+ }
+ }
+
+ return resultRows;
+ }
+ public static void ExecuteSimpleFillRow(object resultRow, out SqlChars rowValue)
+ {
+ var resultRowObject = (ExecuteSimpleRowResult)resultRow;
+ rowValue = new SqlChars(resultRowObject.ResultValue);
+ }
+
+ ///
+ /// Выполнение команды к ClickHouse без получения результата
+ ///
+ /// Строка подключения к ClickHouse
+ /// SQL-текст команды
+ [SqlProcedure]
+ public static void ExecuteStatement(SqlChars connectionString, SqlChars queryText)
+ {
+ string connectionStringValue = new string(connectionString.Value);
+ string queryTextValue = new string(queryText.Value);
+
+ using (var connection = new ClickHouseConnection(connectionStringValue))
+ {
+ var queryResult = connection.ExecuteStatementAsync(queryTextValue)
+ .GetAwaiter().GetResult();
+ }
+ }
+
+ ///
+ /// Функция для получения текста запроса создания временной таблицы
+ /// для сохранения результата запроса к ClickHouse
+ ///
+ /// Строка подключения к ClickHouse
+ /// SQL-текст запроса для выполнения
+ /// Текст SQL-запроса для создания временной таблицы результата запроса
+ [SqlFunction(DataAccess = DataAccessKind.Read)]
+ public static SqlChars GetCreateTempDbTableCommand(SqlChars connectionString, SqlChars queryText, SqlChars tempTableName)
+ {
+ string connectionStringValue = new string(connectionString.Value);
+ string queryTextValue = new string(queryText.Value);
+ // Устанавливаем LIMIT 0, чтобы запрос не возвращал результата.
+ // Используется только для анализа схемы данных.
+ string regexLimitStmt = @"limit[ ][\d]+";
+ if (Regex.IsMatch(queryTextValue, regexLimitStmt, RegexOptions.IgnoreCase))
+ {
+ queryTextValue = Regex.Replace(queryTextValue, regexLimitStmt, "LIMIT 0", RegexOptions.IgnoreCase);
+ } else
+ {
+ queryTextValue = queryTextValue + "\n LIMIT 0";
+ }
+
+ string tempTableNameValue = new string(tempTableName.Value);
+ if (!tempTableNameValue.StartsWith("#", StringComparison.InvariantCultureIgnoreCase))
+ {
+ throw new Exception("Temp table name should begin with # (local temp table) or ## (global temp table)");
+ }
+
+ string resultAsString;
+
+ using (var connection = new ClickHouseConnection(connectionStringValue))
+ {
+ using (var reader = connection.ExecuteReaderAsync(queryTextValue)
+ .GetAwaiter().GetResult())
+ {
+ // Анализ результата запроса и создание под него временной таблицы
+ StringBuilder queryCreateTempTable = new StringBuilder();
+ queryCreateTempTable.Append("CREATE TABLE ");
+ queryCreateTempTable.Append(tempTableNameValue);
+ queryCreateTempTable.Append(" (\n");
+ for (int i = 0; i < reader.FieldCount; i++)
+ {
+ string fieldName = reader.GetName(i);
+ Type fieldType = reader.GetFieldType(i);
+ int fieldNumber = i + 1;
+
+ queryCreateTempTable.Append(" [");
+ queryCreateTempTable.Append(fieldName);
+ queryCreateTempTable.Append("] ");
+ queryCreateTempTable.Append(ConvertClickHouseTypeToSqlType(fieldType, fieldName));
+
+ if (fieldNumber != reader.FieldCount)
+ {
+ queryCreateTempTable.Append(",");
+ }
+
+ queryCreateTempTable.Append("\n");
+ }
+ queryCreateTempTable.Append(")");
+
+ resultAsString = queryCreateTempTable.ToString();
+ }
+ }
+
+ return new SqlChars(resultAsString);
+ }
+
+ ///
+ /// Выполнение запроса к ClickHouse с сохранением результата во временную локальную таблицу
+ ///
+ /// Строка подключения к ClickHouse
+ /// SQL-текст команды
+ /// Имя временной таблицы для сохранения результата
+ [SqlProcedure]
+ public static void ExecuteToTempTable(SqlChars connectionString, SqlChars queryText, SqlChars tempTableName)
+ {
+ string tempTableNameValue = new string(tempTableName.Value);
+
+ if(!tempTableNameValue.StartsWith("#", StringComparison.InvariantCultureIgnoreCase))
+ {
+ throw new Exception("Temp table name should begin with # (local temp table)");
+ }
+ if (tempTableNameValue.StartsWith("##", StringComparison.InvariantCultureIgnoreCase))
+ {
+ throw new Exception("Temp table name should begin with # (local temp table). Global temp table with ## not supported by this method.");
+ }
+
+ ExecuteToTempTableInternal(connectionString, queryText, tempTableName, _emptyString);
+ }
+
+ ///
+ /// Выполнение запроса к ClickHouse с сохранением результата во временную глобальную таблицу
+ ///
+ /// Строка подключения к ClickHouse
+ /// SQL-текст команды
+ /// Имя временной таблицы для сохранения результата
+ /// Строка подключения к SQL Server для выполнения BULK INSERT.
+ /// Если передана пустая строка, то вставка во временную таблицу будет выполняться обычными инструкциями INSERT.
+ ///
+ [SqlProcedure]
+ public static void ExecuteToGlobalTempTable(SqlChars connectionString, SqlChars queryText, SqlChars tempTableName, SqlChars sqlServerConnectionString)
+ {
+ string tempTableNameValue = new string(tempTableName.Value);
+
+ if (!tempTableNameValue.StartsWith("##", StringComparison.InvariantCultureIgnoreCase))
+ {
+ throw new Exception("Temp table name should begin with ## (global temp table).");
+ }
+
+ ExecuteToTempTableInternal(connectionString, queryText, tempTableName, sqlServerConnectionString);
+ }
+
+ ///
+ /// Операция массовой вставки данных из временной таблицы SQL Server
+ /// в таблицу ClickHouse
+ ///
+ /// Строка подключения к ClickHouse
+ /// Имя временной таблицы с исходными данными
+ /// Имя таблицы ClickHouse для вставки данных
+ [SqlProcedure]
+ public static void ExecuteBulkInsertFromTempTable(SqlChars connectionString, SqlChars sourceTempTableName, SqlChars destinationTableName)
+ {
+ string connectionStringValue = new string(connectionString.Value);
+ string sourceTempTableNameValue = new string(sourceTempTableName.Value);
+ string destinationTableNameValue = new string(destinationTableName.Value);
+
+
+ using (SqlConnection sqlConnection = GetSqlConnection())
+ {
+ if (sqlConnection.State != ConnectionState.Open)
+ {
+ sqlConnection.Open();
+ }
+
+ List