diff --git a/Extensions/FreeSql.Generator/ConsoleApp.cs b/Extensions/FreeSql.Generator/ConsoleApp.cs
index 178df09f..e51a1429 100644
--- a/Extensions/FreeSql.Generator/ConsoleApp.cs
+++ b/Extensions/FreeSql.Generator/ConsoleApp.cs
@@ -180,6 +180,7 @@ new Colorful.Formatter("推荐在实体类目录创建 gen.bat,双击它重新
case "dameng": ArgsDbType = DataType.Dameng; break;
case "kingbasees": ArgsDbType = DataType.KingbaseES; break;
case "shentong": ArgsDbType = DataType.ShenTong; break;
+ case "clickhouse": ArgsDbType = DataType.ClickHouse; break;
default: throw new ArgumentException($"-DB 参数错误,不支持的类型:\"{dbargs[0]}\"");
}
ArgsConnectionString = dbargs[1].Trim();
diff --git a/FreeSql.DbContext/DbSet/DbSet.cs b/FreeSql.DbContext/DbSet/DbSet.cs
index 97266db6..a0326c5f 100644
--- a/FreeSql.DbContext/DbSet/DbSet.cs
+++ b/FreeSql.DbContext/DbSet/DbSet.cs
@@ -305,6 +305,7 @@ namespace FreeSql
case DataType.KingbaseES:
case DataType.OdbcKingbaseES:
case DataType.ShenTong:
+ case DataType.ClickHouse:
return true;
default:
if (_tableIdentitys.Length == 1 && _table.Primarys.Length == 1)
@@ -320,6 +321,7 @@ namespace FreeSql
if (isThrow) throw new Exception($"不可添加,已存在于状态管理:{_db.OrmOriginal.GetEntityString(_entityType, data)}");
return false;
}
+ if (_db.OrmOriginal.Ado.DataType == DataType.ClickHouse) return true;
var idval = _db.OrmOriginal.GetEntityIdentityValueWithPrimary(_entityType, data);
if (idval > 0)
{
diff --git a/FreeSql.DbContext/FreeSql.DbContext.xml b/FreeSql.DbContext/FreeSql.DbContext.xml
index da7ace6b..bdd16ff9 100644
--- a/FreeSql.DbContext/FreeSql.DbContext.xml
+++ b/FreeSql.DbContext/FreeSql.DbContext.xml
@@ -538,5 +538,14 @@
+
+
+ 批量注入 Repository,可以参考代码自行调整
+
+
+
+
+
+
diff --git a/FreeSql.Tests/FreeSql.Tests/ClickHouse/ClickHouseTest1.cs b/FreeSql.Tests/FreeSql.Tests/ClickHouse/ClickHouseTest1.cs
new file mode 100644
index 00000000..28f1ca01
--- /dev/null
+++ b/FreeSql.Tests/FreeSql.Tests/ClickHouse/ClickHouseTest1.cs
@@ -0,0 +1,267 @@
+using System;
+using System.Collections.Generic;
+using System.Reflection;
+using System.Text;
+using Xunit;
+using System.Linq;
+using System.Collections;
+using System.Diagnostics;
+using XY.Model.Business;
+
+namespace FreeSql.Tests.MySql
+{
+ public class ClickHouseTest1
+ {
+
+ class TestAuditValue
+ {
+ [FreeSql.DataAnnotations.Column(IsPrimary = true)]
+ public long Id { get; set; }
+ [Now]
+ public DateTime CreateTime { get; set; }
+
+ [FreeSql.DataAnnotations.Column(IsNullable = true )]
+ public string Name { get; set; }
+
+ [FreeSql.DataAnnotations.Column(IsNullable = false)]
+ public int Age { get; set; }
+
+ public bool State { get; set; }
+
+ [FreeSql.DataAnnotations.Column(IsNullable = true)]
+ public bool Enable { get; set; }
+
+ public DateTime? UpdateTime { get; set; }
+
+ public int? Points { get; set; }
+ }
+ [FreeSql.DataAnnotations.Table(Name = "ClickHouseTest")]
+ public class TestClickHouse
+ {
+ [FreeSql.DataAnnotations.Column(IsPrimary = true, IsIdentity = true)]
+ [Now]
+ public long Id { get; set; }
+
+ public string Name { get; set; }
+ public Decimal Money { get; set; }
+ }
+ class NowAttribute: Attribute { }
+
+ [Fact]
+ public void AuditValue()
+ {
+ var id = BitConverter.ToInt64(Guid.NewGuid().ToByteArray(), 0);
+ var item = new TestClickHouse();
+ item.Id = id;
+ item.Name = "李四";
+ EventHandler audit = (s, e) =>
+ {
+ if (e.Property.GetCustomAttribute(false) != null)
+ e.Value = BitConverter.ToInt64(Guid.NewGuid().ToByteArray(), 0);
+ };
+ g.clickHouse.Aop.AuditValue += audit;
+
+ g.clickHouse.Insert(item).ExecuteAffrows();
+
+ g.clickHouse.Aop.AuditValue -= audit;
+ }
+
+
+ [Fact]
+ public void CreateTalbe()
+ {
+ g.clickHouse.CodeFirst.SyncStructure();
+ }
+
+ [Fact]
+ public void TestInsert()
+ {
+ Stopwatch stopwatch =new Stopwatch();
+ var fsql = g.clickHouse;
+ List list=new List();
+ List list1=new List();
+ var date=DateTime.Now;
+ for (int i = 1; i < 1000000; i++)
+ {
+ //list.Add(new TestClickHouse
+ //{
+ // Id=i, Name=i.ToString()
+ //});
+
+ list1.Add(new CollectDataEntity
+ {
+ Id = new Random().Next(),
+ CollectTime = DateTime.Now,
+ DataFlag = "1",
+ EquipmentCode = "11",
+ Guid = "11111",
+ UnitStr = "111",
+ PropertyCode = "1111"
+ });
+ }
+ fsql.Delete().Where(t => 1 == 1).ExecuteAffrows();
+ stopwatch.Start();
+ var insert=fsql.Insert(list1);
+ stopwatch.Stop();
+ Debug.WriteLine("审计数据用时:" + stopwatch.ElapsedMilliseconds.ToString());
+ stopwatch.Restart();
+ insert.ExecuteAffrows();
+ //fsql.GetRepository().Insert(list1);
+ stopwatch.Stop();
+ Debug.WriteLine("转换并插入用时:" +stopwatch.ElapsedMilliseconds.ToString());
+ //var items = fsql.Select().Where(o=>o.Id>900).OrderByDescending(o=>o.Id).ToList();
+ //Assert.Equal(100, items.Count);
+ }
+
+ [Fact]
+ public void TestPage()
+ {
+ var fsql = g.clickHouse;
+
+ var list=fsql.Select()
+ .Page(1,100)
+ .Where(o=>o.Id>200&&o.Id<500)
+ .Count(out var count).ToList();
+ //Assert.Equal(100, list.Count);
+ }
+
+ [Fact]
+ public void TestDelete()
+ {
+ var fsql = g.clickHouse;
+ var count1=fsql.Select().Count();
+ fsql.Delete().Where(o => o.Id < 500).ExecuteAffrows();
+ var count2 = fsql.Select().Count();
+ //Assert.NotEqual(count1, count2);
+ }
+
+ [Fact]
+ public void TestUpdate()
+ {
+ var fsql = g.clickHouse;
+ fsql.Update().Where(o => o.Id > 900)
+ .Set(o=>o.Name,"修改后的值")
+ .ExecuteAffrows();
+
+ }
+
+ [Fact]
+ public void TestRepositorySelect()
+ {
+ var fsql = g.clickHouse;
+ var list=fsql.GetRepository().Where(o => o.Id > 900)
+ .ToList();
+
+ }
+
+ [Fact]
+ public void TestRepositoryInsert()
+ {
+ var fsql = g.clickHouse;
+ long id = BitConverter.ToInt64(Guid.NewGuid().ToByteArray(),0);
+ var list=fsql.GetRepository().Insert(new TestClickHouse { Id= id, Name="张三"});
+ var data=fsql.GetRepository().Get(id);
+ }
+
+ [Fact]
+ public void TestDateTime()
+ {
+ var fsql = g.clickHouse;
+ long id = BitConverter.ToInt64(Guid.NewGuid().ToByteArray(), 0);
+ DateTime createTime=DateTime.Now;
+ fsql.Insert(new TestAuditValue {
+ Id = id, CreateTime = createTime, Age =18,Name="张三"
+ }).ExecuteAffrows();
+
+ var date1 = fsql.GetRepository().Where(o => o.CreateTime == createTime)
+ .ToList();
+ var date2 = fsql.GetRepository().Where(o => o.CreateTime.Date == createTime.Date)
+ .ToList();
+ var date3 = fsql.GetRepository().Where(o => o.CreateTime.Year == createTime.Year)
+ .ToList();
+ var date4 = fsql.GetRepository().Where(o => o.CreateTime.Month == createTime.Month)
+ .ToList();
+ var date5 = fsql.GetRepository().Where(o => o.CreateTime.Second == createTime.Second)
+ .ToList();
+ var date6 = fsql.GetRepository().Where(o => o.CreateTime.Millisecond == createTime.Millisecond)
+ .ToList();
+ var date7 = fsql.GetRepository().Where(o => o.CreateTime.AddSeconds(10) < createTime)
+ .ToList();
+
+ }
+
+
+ [Fact]
+ public void TestUpdateTime()
+ {
+ var fsql = g.clickHouse;
+ var state = fsql.GetRepository().UpdateDiy.Set(o => o.UpdateTime, DateTime.Now).Where(o => 1 == 1).ExecuteAffrows();
+ //var state1 = fsql.GetRepository().UpdateDiy.Set(o => o.UpdateTime, null).Where(o => 1 == 1).ExecuteAffrows();
+
+
+ }
+ [Fact]
+ public void TestRepositoryUpdateTime()
+ {
+ Stopwatch stopwatch = new Stopwatch();
+ var fsql = g.clickHouse;
+ var repository=fsql.GetRepository();
+ List list=new List();
+ for (int i = 1; i < 5; i++)
+ {
+ list.Add(new TestAuditValue
+ {
+ Id = new Random().Next(),
+ Age=1, Name=i.ToString(), State=true, CreateTime=DateTime.Now,
+ UpdateTime=DateTime.Now,
+ Enable = false
+ });
+ }
+ list = repository.Insert(list);
+ //var list = repository.Select.ToList();
+ list.ForEach(o=>o.UpdateTime = DateTime.Now);
+ list.ForEach(o => o.Enable = true);
+ stopwatch.Start();
+ repository.Update(list);
+ stopwatch.Stop();
+ Debug.WriteLine("更新用时:" + stopwatch.ElapsedMilliseconds.ToString());
+
+ }
+ [Fact]
+ public async void TestInsertUpdateData()
+ {
+ //g.clickHouse.CodeFirst.SyncStructure();
+ Stopwatch stopwatch = new Stopwatch();
+ var fsql = g.clickHouse;
+ var repository=fsql.GetRepository();
+ await repository.DeleteAsync(o=>o.Id>0);
+ List tables = new List();
+ for (int i = 1; i < 3; i++)
+ {
+ tables.Add(new CollectDataEntity
+ {
+ Id = new Random().Next(),
+ CollectTime = DateTime.Now,
+ DataFlag = "1",
+ EquipmentCode = "11",
+ UnitStr = "111",
+ PropertyCode = "1111",
+ NumericValue=1111.1119999912500M
+ });
+ }
+
+ var insert = repository.Orm.Insert(tables);
+ insert.ExecuteAffrows();
+ var list = repository.Orm.Select().ToList();
+ //var list = repository.Insert(tables);
+ //var list = repository.Select.ToList();
+ //list.ForEach(o=>o.EquipmentCode = "666");
+ //stopwatch.Start();
+ //await repository.UpdateAsync(list);
+ //stopwatch.Stop();
+ Debug.WriteLine("更新用时:" + stopwatch.ElapsedMilliseconds.ToString());
+
+ }
+
+ }
+}
diff --git a/FreeSql.Tests/FreeSql.Tests/ClickHouse/CollectDataEntity.cs b/FreeSql.Tests/FreeSql.Tests/ClickHouse/CollectDataEntity.cs
new file mode 100644
index 00000000..0e8548e1
--- /dev/null
+++ b/FreeSql.Tests/FreeSql.Tests/ClickHouse/CollectDataEntity.cs
@@ -0,0 +1,168 @@
+using FreeSql.DataAnnotations;
+using Newtonsoft.Json;
+using System;
+using System.ComponentModel;
+namespace XY.Model.Business
+{
+ ///
+ /// 实时数据
+ ///
+ [Table(Name = "CollectData")]
+ [Index("idx_{tablename}_01", nameof(Guid), true)]
+ public partial class CollectDataEntity
+ {
+
+ ///
+ /// Guid
+ ///
+ [Column(StringLength = 50)]
+ public string Guid { get; set; }
+
+ ///
+ /// 租户Id
+ ///
+ [Description("租户Id")]
+ [Column(CanUpdate = false)]
+ public virtual long? TenantId { get; set; }
+
+ ///
+ /// 版本
+ ///
+ [Description("版本")]
+ [Column(IsVersion = false)]
+ public long Version { get; set; }
+
+ ///
+ /// 是否删除
+ ///
+ [Description("是否删除")]
+ [Column()]
+ public bool IsDeleted { get; set; } = false;
+
+ ///
+ /// 创建者Id
+ ///
+ [Description("创建者Id")]
+ [Column(CanUpdate = false)]
+ public long? CreatedUserId { get; set; }
+
+ ///
+ /// 创建者
+ ///
+ [Description("创建者")]
+ [Column(CanUpdate = false, StringLength = 50)]
+ public string CreatedUserName { get; set; }
+
+ ///
+ /// 创建时间
+ ///
+ [Description("创建时间")]
+ [Column(CanUpdate = false, ServerTime = DateTimeKind.Local)]
+ public DateTime? CreatedTime { get; set; }
+
+ ///
+ /// 修改者Id
+ ///
+ [Description("修改者Id")]
+ [Column(CanInsert = false)]
+ public long? ModifiedUserId { get; set; }
+
+ ///
+ /// 修改者
+ ///
+ [Description("修改者")]
+ [Column(CanInsert = false, StringLength = 50)]
+ public string ModifiedUserName { get; set; }
+
+ ///
+ /// 修改时间
+ ///
+ [Description("修改时间")]
+ [Column(CanInsert = false, ServerTime = DateTimeKind.Local)]
+ public DateTime? ModifiedTime { get; set; }
+ ///
+ /// 数据标识
+ ///
+ [Description("数据标识")]
+ [Column(CanInsert = false, StringLength = 2)]
+ public string DataFlag { get; set; }
+ ///
+ /// 主键Id
+ ///
+ [Description("主键Id")]
+ [Column(Position = 1)]
+ public long Id { get; set; }
+ ///
+ /// 设备编号
+ ///
+ [Column(StringLength = 50)]
+ public string EquipmentCode { get; set; }
+
+ ///
+ /// 数据编号,如为空使用默认数据
+ ///
+ [Column(StringLength = 50)]
+ public string PropertyCode { get; set; }
+ ///
+ /// 数据名称,如为空使用默认数据
+ ///
+ [Column(StringLength = 50)]
+ public string PropertyName { get; set; }
+
+ ///
+ /// 数值或状态是否变更
+ ///
+ public bool IsValueOrStateChanged { get; set; }
+
+ ///
+ /// 采集数值
+ ///
+ [Column(StringLength = 18)]
+ public decimal? NumericValue { get; set; }
+
+ ///
+ /// 备注
+ ///
+ [Column(StringLength = 200)]
+ public string Remark { get; set; }
+
+ ///
+ /// 服务标记
+ ///
+ [Column(StringLength = 20)]
+ public string ServiceFlag { get; set; }
+
+ ///
+ /// 状态
+ ///
+ [Column(StringLength = 50)]
+ public string StrState { get; set; }
+
+ ///
+ /// 文本数值
+ ///
+ [Column(StringLength = 50)]
+ public string StrValue { get; set; }
+
+ ///
+ /// 单位
+ ///
+ [Column(StringLength = 10)]
+ public string UnitStr { get; set; }
+
+ ///
+ /// 采集时间
+ ///
+ public DateTime CollectTime { get; set; }
+
+
+ public string FieldKey
+ {
+ get
+ {
+ return EquipmentCode +"_"+ PropertyCode;
+ }
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/FreeSql.Tests/FreeSql.Tests/FreeSql.Tests.csproj b/FreeSql.Tests/FreeSql.Tests/FreeSql.Tests.csproj
index 37eba2fa..a5deb8cb 100644
--- a/FreeSql.Tests/FreeSql.Tests/FreeSql.Tests.csproj
+++ b/FreeSql.Tests/FreeSql.Tests/FreeSql.Tests.csproj
@@ -14,16 +14,16 @@
-
-
-
-
-
-
-
- all
- runtime; build; native; contentfiles; analyzers; buildtransitive
-
+
+
+
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
@@ -37,6 +37,7 @@
+
diff --git a/FreeSql.Tests/FreeSql.Tests/FreeSql.Tests.xml b/FreeSql.Tests/FreeSql.Tests/FreeSql.Tests.xml
index c56f837d..96d17f87 100644
--- a/FreeSql.Tests/FreeSql.Tests/FreeSql.Tests.xml
+++ b/FreeSql.Tests/FreeSql.Tests/FreeSql.Tests.xml
@@ -4,6 +4,11 @@
FreeSql.Tests
+
+
+ 保存或添加,如果主键有值则尝试 Update,如果影响的行为 0 则尝试 Insert
+
+
编号
@@ -287,11 +292,6 @@
修改人
-
-
- 保存或添加,如果主键有值则尝试 Update,如果影响的行为 0 则尝试 Insert
-
-
表中带点
@@ -945,6 +945,126 @@
新打印模块
+
+
+ 实时数据
+
+
+
+
+ Guid
+
+
+
+
+ 租户Id
+
+
+
+
+ 版本
+
+
+
+
+ 是否删除
+
+
+
+
+ 创建者Id
+
+
+
+
+ 创建者
+
+
+
+
+ 创建时间
+
+
+
+
+ 修改者Id
+
+
+
+
+ 修改者
+
+
+
+
+ 修改时间
+
+
+
+
+ 数据标识
+
+
+
+
+ 主键Id
+
+
+
+
+ 设备编号
+
+
+
+
+ 数据编号,如为空使用默认数据
+
+
+
+
+ 数据名称,如为空使用默认数据
+
+
+
+
+ 数值或状态是否变更
+
+
+
+
+ 采集数值
+
+
+
+
+ 备注
+
+
+
+
+ 服务标记
+
+
+
+
+ 状态
+
+
+
+
+ 文本数值
+
+
+
+
+ 单位
+
+
+
+
+ 采集时间
+
+
调价单
diff --git a/FreeSql.Tests/FreeSql.Tests/UnitTest5.cs b/FreeSql.Tests/FreeSql.Tests/UnitTest5.cs
index 90f19686..59d893bc 100644
--- a/FreeSql.Tests/FreeSql.Tests/UnitTest5.cs
+++ b/FreeSql.Tests/FreeSql.Tests/UnitTest5.cs
@@ -34,6 +34,29 @@ namespace FreeSql.Tests
var items = fsql.Select().ToList();
}
+ [Fact]
+ public void TestClickHouse()
+ {
+ var fsql = g.mysql;
+ fsql.Delete().Where("1=1").ExecuteAffrows();
+
+ var item = new TestJsonb01Cls1
+ {
+ jsonb01 = new List { 1, 5, 10, 20 },
+ jsonb02 = new List { 11, 51, 101, 201 },
+ jsonb03 = new List { "12", "52", "102", "202" },
+ };
+ fsql.Insert(item).ExecuteAffrows();
+
+ }
+ [FreeSql.DataAnnotations.Table(Name = "ClickHouseTest")]
+ public class ClickHouse
+ {
+ public long Id { get; set; }
+
+ public string Name { get; set; }
+ }
+
public class TestJsonb01Cls1
{
public Guid id { get; set; }
diff --git a/FreeSql.Tests/FreeSql.Tests/g.cs b/FreeSql.Tests/FreeSql.Tests/g.cs
index 32959df7..df205522 100644
--- a/FreeSql.Tests/FreeSql.Tests/g.cs
+++ b/FreeSql.Tests/FreeSql.Tests/g.cs
@@ -7,6 +7,19 @@ using System.Threading;
public class g
{
+ static Lazy clickHouseLazy = new Lazy(() => new FreeSql.FreeSqlBuilder()
+ .UseConnectionString(FreeSql.DataType.ClickHouse, "Compress=False;BufferSize=32768;SocketTimeout=10000;CheckCompressedHash=False;Encrypt=False;Compressor=lz4;Host=192.168.0.121;Port=8125;Database=PersonnelLocation;Username=root;Password=+riQ8V9D")
+ //.UseConnectionFactory(FreeSql.DataType.MySql, () => new MySql.Data.MySqlClient.MySqlConnection("Data Source=127.0.0.1;Port=3306;User ID=root;Password=root;Initial Catalog=cccddd;Charset=utf8;SslMode=none;"))
+ //.UseConnectionString(FreeSql.DataType.MySql, "Data Source=192.168.164.10;Port=33061;User ID=root;Password=root;Initial Catalog=cccddd_mysqlconnector;Charset=utf8;SslMode=none;Max pool size=10")
+ //.UseAutoSyncStructure(true)
+ //.UseGenerateCommandParameterWithLambda(true)
+ .UseMonitorCommand(
+ cmd => Debug.WriteLine("\r\n线程" + Thread.CurrentThread.ManagedThreadId + ": " + cmd.CommandText) //监听SQL命令对象,在执行前
+ //, (cmd, traceLog) => Console.WriteLine(traceLog)
+ )
+ .UseLazyLoading(true)
+ .Build());
+ public static IFreeSql clickHouse => clickHouseLazy.Value;
static Lazy mysqlLazy = new Lazy(() => new FreeSql.FreeSqlBuilder()
.UseConnectionString(FreeSql.DataType.MySql, "Data Source=127.0.0.1;Port=3306;User ID=root;Password=root;Initial Catalog=cccddd;Charset=utf8;SslMode=none;Max pool size=5;Allow User Variables=True")
diff --git a/FreeSql.sln b/FreeSql.sln
index 6849c8a8..a6d59db1 100644
--- a/FreeSql.sln
+++ b/FreeSql.sln
@@ -1,7 +1,7 @@
Microsoft Visual Studio Solution File, Format Version 12.00
-# Visual Studio Version 16
-VisualStudioVersion = 16.0.29324.140
+# Visual Studio Version 17
+VisualStudioVersion = 17.0.31903.59
MinimumVisualStudioVersion = 15.0.26124.0
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FreeSql", "FreeSql\FreeSql.csproj", "{AF9C50EC-6EB6-494B-9B3B-7EDBA6FD0EBB}"
EndProject
@@ -101,6 +101,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
README.zh-CN.md = README.zh-CN.md
EndProjectSection
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FreeSql.Provider.ClickHouse", "Providers\FreeSql.Provider.ClickHouse\FreeSql.Provider.ClickHouse.csproj", "{86C56235-5D37-4422-807B-B31681C7D01C}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -579,6 +581,18 @@ Global
{0DBAA21C-39B2-4AAD-A43D-88E67ED239D1}.Release|x64.Build.0 = Release|Any CPU
{0DBAA21C-39B2-4AAD-A43D-88E67ED239D1}.Release|x86.ActiveCfg = Release|Any CPU
{0DBAA21C-39B2-4AAD-A43D-88E67ED239D1}.Release|x86.Build.0 = Release|Any CPU
+ {86C56235-5D37-4422-807B-B31681C7D01C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {86C56235-5D37-4422-807B-B31681C7D01C}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {86C56235-5D37-4422-807B-B31681C7D01C}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {86C56235-5D37-4422-807B-B31681C7D01C}.Debug|x64.Build.0 = Debug|Any CPU
+ {86C56235-5D37-4422-807B-B31681C7D01C}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {86C56235-5D37-4422-807B-B31681C7D01C}.Debug|x86.Build.0 = Debug|Any CPU
+ {86C56235-5D37-4422-807B-B31681C7D01C}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {86C56235-5D37-4422-807B-B31681C7D01C}.Release|Any CPU.Build.0 = Release|Any CPU
+ {86C56235-5D37-4422-807B-B31681C7D01C}.Release|x64.ActiveCfg = Release|Any CPU
+ {86C56235-5D37-4422-807B-B31681C7D01C}.Release|x64.Build.0 = Release|Any CPU
+ {86C56235-5D37-4422-807B-B31681C7D01C}.Release|x86.ActiveCfg = Release|Any CPU
+ {86C56235-5D37-4422-807B-B31681C7D01C}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -612,6 +626,7 @@ Global
{CDD6A896-F6DF-44CB-B430-06B383916EB0} = {2A381C57-2697-427B-9F10-55DA11FD02E4}
{101B11D2-7780-4E14-9B72-77F5D69B3DF9} = {2A381C57-2697-427B-9F10-55DA11FD02E4}
{0DBAA21C-39B2-4AAD-A43D-88E67ED239D1} = {2A381C57-2697-427B-9F10-55DA11FD02E4}
+ {86C56235-5D37-4422-807B-B31681C7D01C} = {2A381C57-2697-427B-9F10-55DA11FD02E4}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {089687FD-5D25-40AB-BA8A-A10D1E137F98}
diff --git a/FreeSql/DataType.cs b/FreeSql/DataType.cs
index fb574578..f2c978ea 100644
--- a/FreeSql/DataType.cs
+++ b/FreeSql/DataType.cs
@@ -60,6 +60,7 @@ namespace FreeSql
/// 自定义适配器,访问任何数据库
/// 注意:该类型不提供 DbFirst/CodeFirst 功能
///
- Custom
+ Custom,
+ ClickHouse
}
}
diff --git a/FreeSql/Extensions/AdoNetExtensions.cs b/FreeSql/Extensions/AdoNetExtensions.cs
index 3e27483b..73b0d015 100644
--- a/FreeSql/Extensions/AdoNetExtensions.cs
+++ b/FreeSql/Extensions/AdoNetExtensions.cs
@@ -59,6 +59,10 @@ namespace FreeSql
providerType = Type.GetType("FreeSql.Firebird.FirebirdProvider`1,FreeSql.Provider.Firebird")?.MakeGenericType(connType);
if (providerType == null) throw new Exception("缺少 FreeSql 数据库实现包:FreeSql.Provider.Firebird.dll,可前往 nuget 下载");
break;
+ case "ClickHouseConnection":
+ providerType = Type.GetType("FreeSql.ClickHouse.ClickHouseProvider`1,FreeSql.Provider.ClickHouse")?.MakeGenericType(connType);
+ if (providerType == null) throw new Exception("缺少 FreeSql 数据库实现包:FreeSql.Provider.ClickHouse.dll,可前往 nuget 下载");
+ break;
default:
throw new Exception("未实现");
}
diff --git a/FreeSql/Extensions/FreeSqlGlobalExtensions.cs b/FreeSql/Extensions/FreeSqlGlobalExtensions.cs
index b96ea0c2..3f67a8a5 100644
--- a/FreeSql/Extensions/FreeSqlGlobalExtensions.cs
+++ b/FreeSql/Extensions/FreeSqlGlobalExtensions.cs
@@ -58,7 +58,7 @@ public static partial class FreeSqlGlobalExtensions
///
///
///
- internal static string DisplayCsharp(this Type type, bool isNameSpace = true)
+ public static string DisplayCsharp(this Type type, bool isNameSpace = true)
{
if (type == null) return null;
if (type == typeof(void)) return "void";
@@ -495,6 +495,7 @@ JOIN {select._commonUtils.QuoteSqlName(tb.DbName)} a ON cte_tbc.cte_id = a.{sele
case DataType.SqlServer:
case DataType.OdbcSqlServer:
case DataType.Firebird:
+ case DataType.ClickHouse:
sql1ctePath = select._commonExpression.ExpressionWhereLambda(select._tables, Expression.Call(typeof(Convert).GetMethod("ToString", new Type[] { typeof(string) }), pathSelector?.Body), null, null, null);
break;
default:
@@ -609,6 +610,7 @@ SELECT ");
{
case DataType.MySql:
case DataType.OdbcMySql:
+ case DataType.ClickHouse:
return that.OrderBy("rand()");
case DataType.SqlServer:
case DataType.OdbcSqlServer:
diff --git a/FreeSql/FreeSql.csproj b/FreeSql/FreeSql.csproj
index 40d58264..4d45126e 100644
--- a/FreeSql/FreeSql.csproj
+++ b/FreeSql/FreeSql.csproj
@@ -1,7 +1,7 @@
- netstandard2.0;net45;net40
+ netstandard2.1;netstandard2.0;net45;net40
2.6.100
true
FreeSql;ncc;YeXiangQin
diff --git a/FreeSql/FreeSqlBuilder.cs b/FreeSql/FreeSqlBuilder.cs
index 344e2a2c..0bb2383f 100644
--- a/FreeSql/FreeSqlBuilder.cs
+++ b/FreeSql/FreeSqlBuilder.cs
@@ -265,6 +265,11 @@ namespace FreeSql
if (type == null) throwNotFind("FreeSql.Provider.Custom.dll", "FreeSql.Custom.CustomProvider<>");
break;
+ case DataType.ClickHouse:
+ type = Type.GetType("FreeSql.ClickHouse.ClickHouseProvider`1,FreeSql.Provider.ClickHouse")?.MakeGenericType(typeof(TMark));
+ if (type == null) throwNotFind("FreeSql.Provider.ClickHouse.dll", "FreeSql.ClickHouse.ClickHouseProvider<>");
+ break;
+
default: throw new Exception("未指定 UseConnectionString 或者 UseConnectionFactory");
}
}
diff --git a/FreeSql/Internal/CommonProvider/DeleteProvider.cs b/FreeSql/Internal/CommonProvider/DeleteProvider.cs
index 95d92df9..652623db 100644
--- a/FreeSql/Internal/CommonProvider/DeleteProvider.cs
+++ b/FreeSql/Internal/CommonProvider/DeleteProvider.cs
@@ -157,7 +157,7 @@ namespace FreeSql.Internal.CommonProvider
return this;
}
- public string ToSql()
+ public virtual string ToSql()
{
if (_whereTimes <= 0) return null;
var sb = new StringBuilder().Append("DELETE FROM ").Append(_commonUtils.QuoteSqlName(TableRuleInvoke())).Append(" WHERE ").Append(_where);
diff --git a/FreeSql/Internal/CommonProvider/InsertProvider.cs b/FreeSql/Internal/CommonProvider/InsertProvider.cs
index 5a2b62cb..21254fc0 100644
--- a/FreeSql/Internal/CommonProvider/InsertProvider.cs
+++ b/FreeSql/Internal/CommonProvider/InsertProvider.cs
@@ -210,7 +210,7 @@ namespace FreeSql.Internal.CommonProvider
ret[a] = _source.GetRange(a * takeMax, Math.Min(takeMax, _source.Count - a * takeMax));
return ret;
}
- protected int SplitExecuteAffrows(int valuesLimit, int parameterLimit)
+ protected virtual int SplitExecuteAffrows(int valuesLimit, int parameterLimit)
{
var ss = SplitSource(valuesLimit, parameterLimit);
var ret = 0;
diff --git a/FreeSql/Internal/CommonProvider/InsertProviderAsync.cs b/FreeSql/Internal/CommonProvider/InsertProviderAsync.cs
index 828dba7d..1dfefd6b 100644
--- a/FreeSql/Internal/CommonProvider/InsertProviderAsync.cs
+++ b/FreeSql/Internal/CommonProvider/InsertProviderAsync.cs
@@ -17,7 +17,7 @@ namespace FreeSql.Internal.CommonProvider
{
#if net40
#else
- async protected Task SplitExecuteAffrowsAsync(int valuesLimit, int parameterLimit, CancellationToken cancellationToken = default)
+ async protected virtual Task SplitExecuteAffrowsAsync(int valuesLimit, int parameterLimit, CancellationToken cancellationToken = default)
{
var ss = SplitSource(valuesLimit, parameterLimit);
var ret = 0;
@@ -96,7 +96,7 @@ namespace FreeSql.Internal.CommonProvider
return ret;
}
- async protected Task SplitExecuteIdentityAsync(int valuesLimit, int parameterLimit, CancellationToken cancellationToken = default)
+ async protected virtual Task SplitExecuteIdentityAsync(int valuesLimit, int parameterLimit, CancellationToken cancellationToken = default)
{
var ss = SplitSource(valuesLimit, parameterLimit);
long ret = 0;
@@ -177,7 +177,7 @@ namespace FreeSql.Internal.CommonProvider
return ret;
}
- async protected Task> SplitExecuteInsertedAsync(int valuesLimit, int parameterLimit, CancellationToken cancellationToken = default)
+ async protected virtual Task> SplitExecuteInsertedAsync(int valuesLimit, int parameterLimit, CancellationToken cancellationToken = default)
{
var ss = SplitSource(valuesLimit, parameterLimit);
var ret = new List();
diff --git a/FreeSql/Internal/CommonProvider/UpdateProvider.cs b/FreeSql/Internal/CommonProvider/UpdateProvider.cs
index 2fad2154..63b81baf 100644
--- a/FreeSql/Internal/CommonProvider/UpdateProvider.cs
+++ b/FreeSql/Internal/CommonProvider/UpdateProvider.cs
@@ -138,7 +138,7 @@ namespace FreeSql.Internal.CommonProvider
}
#region 参数化数据限制,或values数量限制
- internal List[] SplitSource(int valuesLimit, int parameterLimit)
+ protected internal List[] SplitSource(int valuesLimit, int parameterLimit)
{
valuesLimit = valuesLimit - 1;
parameterLimit = parameterLimit - 1;
@@ -163,7 +163,7 @@ namespace FreeSql.Internal.CommonProvider
ret[a] = _source.GetRange(a * takeMax, Math.Min(takeMax, _source.Count - a * takeMax));
return ret;
}
- protected int SplitExecuteAffrows(int valuesLimit, int parameterLimit)
+ protected virtual int SplitExecuteAffrows(int valuesLimit, int parameterLimit)
{
var ss = SplitSource(valuesLimit, parameterLimit);
var ret = 0;
@@ -237,7 +237,7 @@ namespace FreeSql.Internal.CommonProvider
return ret;
}
- protected List SplitExecuteUpdated(int valuesLimit, int parameterLimit)
+ protected virtual List SplitExecuteUpdated(int valuesLimit, int parameterLimit)
{
var ss = SplitSource(valuesLimit, parameterLimit);
var ret = new List();
@@ -445,7 +445,7 @@ namespace FreeSql.Internal.CommonProvider
_set.Append(", ").Append(_commonUtils.QuoteSqlName(col.Attribute.Name)).Append(" = ");
var colsql = _noneParameter ? _commonUtils.GetNoneParamaterSqlValue(_params, "u", col, col.Attribute.MapType, val) :
- _commonUtils.QuoteWriteParamterAdapter(col.Attribute.MapType, $"{_commonUtils.QuoteParamterName("p_")}{_params.Count}");
+ _commonUtils.QuoteWriteParamterAdapter(col.Attribute.MapType, _commonUtils.QuoteParamterName($"p_{_params.Count}"));
_set.Append(_commonUtils.RewriteColumn(col, colsql));
if (_noneParameter == false)
_commonUtils.AppendParamter(_params, null, col, col.Attribute.MapType, val);
diff --git a/FreeSql/Internal/CommonProvider/UpdateProviderAsync.cs b/FreeSql/Internal/CommonProvider/UpdateProviderAsync.cs
index 32e97a6c..9f8aa53a 100644
--- a/FreeSql/Internal/CommonProvider/UpdateProviderAsync.cs
+++ b/FreeSql/Internal/CommonProvider/UpdateProviderAsync.cs
@@ -18,7 +18,7 @@ namespace FreeSql.Internal.CommonProvider
{
#if net40
#else
- async protected Task SplitExecuteAffrowsAsync(int valuesLimit, int parameterLimit, CancellationToken cancellationToken = default)
+ async protected virtual Task SplitExecuteAffrowsAsync(int valuesLimit, int parameterLimit, CancellationToken cancellationToken = default)
{
var ss = SplitSource(valuesLimit, parameterLimit);
var ret = 0;
@@ -91,7 +91,7 @@ namespace FreeSql.Internal.CommonProvider
ClearData();
return ret;
}
- async protected Task> SplitExecuteUpdatedAsync(int valuesLimit, int parameterLimit, CancellationToken cancellationToken = default)
+ async protected virtual Task> SplitExecuteUpdatedAsync(int valuesLimit, int parameterLimit, CancellationToken cancellationToken = default)
{
var ss = SplitSource(valuesLimit, parameterLimit);
var ret = new List();
diff --git a/FreeSql/Internal/CommonUtils.cs b/FreeSql/Internal/CommonUtils.cs
index 63bd8905..93be8c72 100644
--- a/FreeSql/Internal/CommonUtils.cs
+++ b/FreeSql/Internal/CommonUtils.cs
@@ -54,7 +54,7 @@ namespace FreeSql.Internal
public abstract string NowUtc { get; }
public abstract string QuoteWriteParamterAdapter(Type type, string paramterName);
protected abstract string QuoteReadColumnAdapter(Type type, Type mapType, string columnName);
- public string RewriteColumn(ColumnInfo col, string sql)
+ public virtual string RewriteColumn(ColumnInfo col, string sql)
{
if (string.IsNullOrWhiteSpace(col?.Attribute.RewriteSql) == false)
return string.Format(col.Attribute.RewriteSql, sql);
diff --git a/FreeSql/Internal/UtilsExpressionTree.cs b/FreeSql/Internal/UtilsExpressionTree.cs
index ffe5d765..9db2a0c3 100644
--- a/FreeSql/Internal/UtilsExpressionTree.cs
+++ b/FreeSql/Internal/UtilsExpressionTree.cs
@@ -118,7 +118,7 @@ namespace FreeSql.Internal
var leftBt = colattr.DbType.IndexOf('(');
colattr.DbType = colattr.DbType.Substring(0, leftBt).ToUpper() + colattr.DbType.Substring(leftBt);
}
- else
+ else if(common._orm.Ado.DataType != DataType.ClickHouse)
colattr.DbType = colattr.DbType.ToUpper();
if (colattrIsNull == false && colattrIsNullable == true) colattr.DbType = colattr.DbType.Replace("NOT NULL", "");
@@ -129,12 +129,13 @@ namespace FreeSql.Internal
if (common.CodeFirst.IsSyncStructureToLower) colattr.Name = colattr.Name.ToLower();
if (common.CodeFirst.IsSyncStructureToUpper) colattr.Name = colattr.Name.ToUpper();
- if ((colattr.IsNullable != true || colattr.IsIdentity == true || colattr.IsPrimary == true) && colattr.DbType.Contains("NOT NULL") == false)
+ if ((colattr.IsNullable != true || colattr.IsIdentity == true || colattr.IsPrimary == true) && colattr.DbType.Contains("NOT NULL") == false && common._orm.Ado.DataType != DataType.ClickHouse)
{
colattr.IsNullable = false;
colattr.DbType = Regex.Replace(colattr.DbType, @"\bNULL\b", "").Trim() + " NOT NULL";
}
if (colattr.IsNullable == true && colattr.DbType.Contains("NOT NULL")) colattr.DbType = colattr.DbType.Replace("NOT NULL", "");
+ else if (colattr.IsNullable == true && !colattr.DbType.Contains("Nullable") && common._orm.Ado.DataType == DataType.ClickHouse)colattr.DbType = $"Nullable({colattr.DbType})" ;
colattr.DbType = Regex.Replace(colattr.DbType, @"\([^\)]+\)", m =>
{
var tmpLt = Regex.Replace(m.Groups[0].Value, @"\s", "");
diff --git a/Providers/FreeSql.Provider.ClickHouse/ClickHouseAdo/ClickHouseAdo.cs b/Providers/FreeSql.Provider.ClickHouse/ClickHouseAdo/ClickHouseAdo.cs
new file mode 100644
index 00000000..0eb76626
--- /dev/null
+++ b/Providers/FreeSql.Provider.ClickHouse/ClickHouseAdo/ClickHouseAdo.cs
@@ -0,0 +1,79 @@
+using FreeSql.Internal;
+using FreeSql.Internal.Model;
+using FreeSql.Internal.ObjectPool;
+using System;
+using System.Collections;
+using System.Data.Common;
+using System.Text;
+using System.Threading;
+using ClickHouse.Client.ADO;
+
+namespace FreeSql.ClickHouse
+{
+ class ClickHouseAdo : FreeSql.Internal.CommonProvider.AdoProvider
+ {
+
+ public ClickHouseAdo() : base(DataType.ClickHouse, null, null) { }
+ public ClickHouseAdo(CommonUtils util, string masterConnectionString, string[] slaveConnectionStrings, Func connectionFactory) : base(DataType.ClickHouse, masterConnectionString, slaveConnectionStrings)
+ {
+ base._util = util;
+ if (connectionFactory != null)
+ {
+ MasterPool = new FreeSql.Internal.CommonProvider.DbConnectionPool(DataType.ClickHouse, connectionFactory);
+ return;
+ }
+ if (!string.IsNullOrEmpty(masterConnectionString))
+ MasterPool = new ClickHouseConnectionPool("主库", masterConnectionString, null, null);
+ if (slaveConnectionStrings != null)
+ {
+ foreach (var slaveConnectionString in slaveConnectionStrings)
+ {
+ var slavePool = new ClickHouseConnectionPool($"从库{SlavePools.Count + 1}", slaveConnectionString, () => Interlocked.Decrement(ref slaveUnavailables), () => Interlocked.Increment(ref slaveUnavailables));
+ SlavePools.Add(slavePool);
+ }
+ }
+ }
+ public override object AddslashesProcessParam(object param, Type mapType, ColumnInfo mapColumn)
+ {
+ if (param == null) return "NULL";
+ if (mapType != null && mapType != param.GetType() && (param is IEnumerable == false))
+ param = Utils.GetDataReaderValue(mapType, param);
+
+ if (param is bool || param is bool?)
+ return (bool)param ? 1 : 0;
+ else if (param is string)
+ return string.Concat("'", param.ToString().Replace("'", "''").Replace("\\", "\\\\"), "'"); //只有 mysql 需要处理反斜杠
+ else if (param is char)
+ return string.Concat("'", param.ToString().Replace("'", "''").Replace("\\", "\\\\").Replace('\0', ' '), "'");
+ else if (param is Enum)
+ return string.Concat("'", param.ToString().Replace("'", "''").Replace("\\", "\\\\"), "'"); //((Enum)val).ToInt64();
+ else if (decimal.TryParse(string.Concat(param), out var trydec))
+ return param;
+ else if (param is DateTime || param is DateTime?)
+ return string.Concat("'", ((DateTime)param).ToString("yyyy-MM-dd HH:mm:ss"), "'");
+ else if (param is TimeSpan || param is TimeSpan?)
+ return ((TimeSpan)param).Ticks / 10;
+ else if (param is byte[])
+ return $"0x{CommonUtils.BytesSqlRaw(param as byte[])}";
+ else if (param is IEnumerable)
+ return AddslashesIEnumerable(param, mapType, mapColumn);
+
+ return string.Concat("'", param.ToString().Replace("'", "''").Replace("\\", "\\\\"), "'");
+ }
+
+ public override DbCommand CreateCommand()
+ {
+ System.Data.IDbCommand command = new ClickHouseCommand();
+ return (DbCommand)command;
+ }
+
+ public override void ReturnConnection(IObjectPool pool, Object conn, Exception ex)
+ {
+ var rawPool = pool as ClickHouseConnectionPool;
+ if (rawPool != null) rawPool.Return(conn, ex);
+ else pool.Return(conn);
+ }
+
+ public override DbParameter[] GetDbParamtersByObject(string sql, object obj) => _util.GetDbParamtersByObject(sql, obj);
+ }
+}
diff --git a/Providers/FreeSql.Provider.ClickHouse/ClickHouseAdo/ClickHouseConnectionPool.cs b/Providers/FreeSql.Provider.ClickHouse/ClickHouseAdo/ClickHouseConnectionPool.cs
new file mode 100644
index 00000000..b217c586
--- /dev/null
+++ b/Providers/FreeSql.Provider.ClickHouse/ClickHouseAdo/ClickHouseConnectionPool.cs
@@ -0,0 +1,257 @@
+using FreeSql.Internal.ObjectPool;
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Data;
+using System.Data.Common;
+using System.Text.RegularExpressions;
+using System.Threading.Tasks;
+using ClickHouse.Client.ADO;
+
+namespace FreeSql.ClickHouse
+{
+
+ class ClickHouseConnectionPool : ObjectPool
+ {
+
+ internal Action availableHandler;
+ internal Action unavailableHandler;
+
+ public ClickHouseConnectionPool(string name, string connectionString, Action availableHandler, Action unavailableHandler) : base(null)
+ {
+ this.availableHandler = availableHandler;
+ this.unavailableHandler = unavailableHandler;
+ var policy = new ClickHouseConnectionPoolPolicy
+ {
+ _pool = this,
+ Name = name
+ };
+ this.Policy = policy;
+ policy.ConnectionString = connectionString;
+ }
+
+ public void Return(Object obj, Exception exception, bool isRecreate = false)
+ {
+ if (exception != null && exception is ClickHouseException)
+ {
+ try { if (obj.Value.Ping() == false) obj.Value.Open(); } catch { base.SetUnavailable(exception); }
+ }
+ base.Return(obj, isRecreate);
+ }
+ }
+ public class ClickHouseException : Exception
+ {
+ public int Code;
+
+ public string Name;
+ public string ServerStackTrace;
+
+ public ClickHouseException() { }
+
+ public ClickHouseException(string message) : base(message) { }
+
+ public ClickHouseException(string message, Exception innerException) : base(message, innerException) { }
+ }
+ class ClickHouseConnectionPoolPolicy : IPolicy
+ {
+
+ internal ClickHouseConnectionPool _pool;
+ public string Name { get; set; } = "ClickHouse ClickHouseConnection 对象池";
+ public int PoolSize { get; set; } = 100;
+ public TimeSpan SyncGetTimeout { get; set; } = TimeSpan.FromSeconds(10);
+ public TimeSpan IdleTimeout { get; set; } = TimeSpan.FromSeconds(20);
+ public int AsyncGetCapacity { get; set; } = 10000;
+ public bool IsThrowGetTimeoutException { get; set; } = true;
+ public bool IsAutoDisposeWithSystem { get; set; } = true;
+ public int CheckAvailableInterval { get; set; } = 5;
+
+ static ConcurrentDictionary dicConnStrIncr = new ConcurrentDictionary(StringComparer.CurrentCultureIgnoreCase);
+ private string _connectionString;
+ public string ConnectionString
+ {
+ get => _connectionString;
+ set
+ {
+ _connectionString = value ?? "";
+
+ //var pattern = @"Max\s*pool\s*size\s*=\s*(\d+)";
+ //var m = Regex.Match(_connectionString, pattern, RegexOptions.IgnoreCase);
+ //if (m.Success == false || int.TryParse(m.Groups[1].Value, out var poolsize) == false || poolsize <= 0) poolsize = 100;
+ //var connStrIncr = dicConnStrIncr.AddOrUpdate(_connectionString, 1, (oldkey, oldval) => Math.Min(5, oldval + 1));
+ //PoolSize = poolsize + connStrIncr;
+ //_connectionString = m.Success ?
+ // Regex.Replace(_connectionString, pattern, $"Max pool size={PoolSize}", RegexOptions.IgnoreCase) :
+ // $"{_connectionString};Max pool size={PoolSize}";
+
+
+ var pattern = @"Max\s*pool\s*size\s*=\s*(\d+)";
+ Match m = Regex.Match(_connectionString, pattern, RegexOptions.IgnoreCase);
+ if (m.Success)
+ {
+ PoolSize = int.Parse(m.Groups[1].Value);
+ _connectionString = Regex.Replace(_connectionString, pattern, "", RegexOptions.IgnoreCase);
+ }
+
+ pattern = @"Connection\s*LifeTime\s*=\s*(\d+)";
+ m = Regex.Match(_connectionString, pattern, RegexOptions.IgnoreCase);
+ if (m.Success)
+ {
+ IdleTimeout = TimeSpan.FromSeconds(int.Parse(m.Groups[1].Value));
+ _connectionString = Regex.Replace(_connectionString, pattern, "", RegexOptions.IgnoreCase);
+ }
+
+ var minPoolSize = 0;
+ pattern = @"Min\s*pool\s*size\s*=\s*(\d+)";
+ m = Regex.Match(_connectionString, pattern, RegexOptions.IgnoreCase);
+ if (m.Success)
+ {
+ minPoolSize = int.Parse(m.Groups[1].Value);
+ _connectionString = Regex.Replace(_connectionString, pattern, "", RegexOptions.IgnoreCase);
+ }
+
+ FreeSql.Internal.CommonUtils.PrevReheatConnectionPool(_pool, minPoolSize);
+ }
+ }
+
+ public bool OnCheckAvailable(Object obj)
+ {
+ if (obj.Value.State == ConnectionState.Closed) obj.Value.Open();
+ return obj.Value.Ping(true);
+ }
+
+ public DbConnection OnCreate()
+ {
+ var conn = new ClickHouseConnection(_connectionString);
+ return conn;
+ }
+
+ public void OnDestroy(DbConnection obj)
+ {
+ if (obj.State != ConnectionState.Closed) obj.Close();
+ obj.Dispose();
+ }
+
+ public void OnGet(Object obj)
+ {
+
+ if (_pool.IsAvailable)
+ {
+ if (obj.Value == null)
+ {
+ if (_pool.SetUnavailable(new Exception("连接字符串错误")) == true)
+ throw new Exception($"【{this.Name}】连接字符串错误,请检查。");
+ return;
+ }
+
+ if (obj.Value.State != ConnectionState.Open || DateTime.Now.Subtract(obj.LastReturnTime).TotalSeconds > 60 && obj.Value.Ping() == false)
+ {
+
+ try
+ {
+ obj.Value.Open();
+ }
+ catch (Exception ex)
+ {
+ if (_pool.SetUnavailable(ex) == true)
+ throw new Exception($"【{this.Name}】状态不可用,等待后台检查程序恢复方可使用。{ex.Message}");
+ }
+ }
+ }
+ }
+
+#if net40
+#else
+ async public Task OnGetAsync(Object obj)
+ {
+
+ if (_pool.IsAvailable)
+ {
+ if (obj.Value == null)
+ {
+ if (_pool.SetUnavailable(new Exception("连接字符串错误")) == true)
+ throw new Exception($"【{this.Name}】连接字符串错误,请检查。");
+ return;
+ }
+
+ if (obj.Value.State != ConnectionState.Open || DateTime.Now.Subtract(obj.LastReturnTime).TotalSeconds > 60 && (await obj.Value.PingAsync()) == false)
+ {
+
+ try
+ {
+ await obj.Value.OpenAsync();
+ }
+ catch (Exception ex)
+ {
+ if (_pool.SetUnavailable(ex) == true)
+ throw new Exception($"【{this.Name}】状态不可用,等待后台检查程序恢复方可使用。{ex.Message}");
+ }
+ }
+ }
+ }
+#endif
+
+ public void OnGetTimeout()
+ {
+
+ }
+
+ public void OnReturn(Object obj)
+ {
+ //if (obj?.Value != null && obj.Value.State != ConnectionState.Closed) try { obj.Value.Close(); } catch { }
+ }
+
+ public void OnAvailable()
+ {
+ _pool.availableHandler?.Invoke();
+ }
+
+ public void OnUnavailable()
+ {
+ _pool.unavailableHandler?.Invoke();
+ }
+ }
+
+ static class DbConnectionExtensions
+ {
+
+ static DbCommand PingCommand(DbConnection conn)
+ {
+ var cmd = conn.CreateCommand();
+ cmd.CommandTimeout = 5;
+ cmd.CommandText = "select 1";
+ return cmd;
+ }
+ public static bool Ping(this DbConnection that, bool isThrow = false)
+ {
+ try
+ {
+ PingCommand(that).ExecuteNonQuery();
+ return true;
+ }
+ catch
+ {
+ if (that.State != ConnectionState.Closed) try { that.Close(); } catch { }
+ if (isThrow) throw;
+ return false;
+ }
+ }
+
+#if net40
+#else
+ async public static Task PingAsync(this DbConnection that, bool isThrow = false)
+ {
+ try
+ {
+ await PingCommand(that).ExecuteNonQueryAsync();
+ return true;
+ }
+ catch
+ {
+ if (that.State != ConnectionState.Closed) try { that.Close(); } catch { }
+ if (isThrow) throw;
+ return false;
+ }
+ }
+#endif
+ }
+}
diff --git a/Providers/FreeSql.Provider.ClickHouse/ClickHouseCodeFirst.cs b/Providers/FreeSql.Provider.ClickHouse/ClickHouseCodeFirst.cs
new file mode 100644
index 00000000..8f006685
--- /dev/null
+++ b/Providers/FreeSql.Provider.ClickHouse/ClickHouseCodeFirst.cs
@@ -0,0 +1,332 @@
+using FreeSql.Internal;
+using FreeSql.Internal.Model;
+using System;
+using System.Collections.Generic;
+using System.Data;
+using System.Linq;
+using System.Text;
+using System.Text.RegularExpressions;
+using System.Data.Common;
+using FreeSql.Internal.ObjectPool;
+using ClickHouse.Client.ADO;
+
+namespace FreeSql.ClickHouse
+{
+
+ class ClickHouseCodeFirst : Internal.CommonProvider.CodeFirstProvider
+ {
+
+ public ClickHouseCodeFirst(IFreeSql orm, CommonUtils commonUtils, CommonExpression commonExpression) : base(orm, commonUtils, commonExpression) { }
+
+ static object _dicCsToDbLock = new object();
+
+ static Dictionary> _dicCsToDb = new Dictionary>() {
+ { typeof(bool).FullName, CsToDb.New(DbType.SByte, "Int8","Int8", null, false, false) },{ typeof(bool?).FullName, CsToDb.New(DbType.SByte, "Int8","Nullable(Int8)", null, true, null) },
+
+ { typeof(sbyte).FullName, CsToDb.New(DbType.SByte, "Int8", "Int8", false, false, 0) },{ typeof(sbyte?).FullName, CsToDb.New(DbType.SByte, "Int8", "Nullable(Int8)", false, true, null) },
+ { typeof(short).FullName, CsToDb.New(DbType.Int16, "Int16","Int16", false, false, 0) },{ typeof(short?).FullName, CsToDb.New(DbType.Int16, "Int16", "Nullable(Int16)", false, true, null) },
+ { typeof(int).FullName, CsToDb.New(DbType.Int32, "Int32", "Int32", false, false, 0) },{ typeof(int?).FullName, CsToDb.New(DbType.Int32, "Int32", "Nullable(Int32)", false, true, null) },
+ { typeof(long).FullName, CsToDb.New(DbType.Int64, "Int64","Int64", false, false, 0) },{ typeof(long?).FullName, CsToDb.New(DbType.Int64, "Int64","Nullable(Int64)", false, true, null) },
+
+ { typeof(byte).FullName, CsToDb.New(DbType.Byte, "UInt8","UInt8", true, false, 0) },{ typeof(byte?).FullName, CsToDb.New(DbType.Byte, "UInt8","Nullable(UInt8)", true, true, null) },
+ { typeof(ushort).FullName, CsToDb.New(DbType.UInt16, "UInt16","UInt16", true, false, 0) },{ typeof(ushort?).FullName, CsToDb.New(DbType.UInt16, "UInt16", "Nullable(UInt16)", true, true, null) },
+ { typeof(uint).FullName, CsToDb.New(DbType.UInt32, "UInt32", "UInt32", true, false, 0) },{ typeof(uint?).FullName, CsToDb.New(DbType.UInt32, "UInt32", "Nullable(UInt32)", true, true, null) },
+ { typeof(ulong).FullName, CsToDb.New(DbType.UInt64, "UInt64", "UInt64", true, false, 0) },{ typeof(ulong?).FullName, CsToDb.New(DbType.UInt64, "UInt64", "Nullable(UInt64)", true, true, null) },
+
+ { typeof(double).FullName, CsToDb.New(DbType.Double, "Float64", "Float64", false, false, 0) },{ typeof(double?).FullName, CsToDb.New(DbType.Double, "Float64", "Nullable(Float64)", false, true, null) },
+ { typeof(float).FullName, CsToDb.New(DbType.Single, "Float32","Float32", false, false, 0) },{ typeof(float?).FullName, CsToDb.New(DbType.Single, "Float32","Nullable(Float32)", false, true, null) },
+ { typeof(decimal).FullName, CsToDb.New(DbType.Decimal, "Decimal128(19)","Decimal128(19)", false, false, 0) },{ typeof(decimal?).FullName, CsToDb.New(DbType.Decimal, "Nullable(Decimal128(19))","Nullable(Decimal128(19))", false, true, null) },
+
+ { typeof(DateTime).FullName, CsToDb.New(DbType.DateTime, "DateTime('Asia/Shanghai')", "DateTime('Asia/Shanghai')", false, false, new DateTime(1970,1,1)) },{ typeof(DateTime?).FullName, CsToDb.New(DbType.DateTime, "DateTime('Asia/Shanghai')", "Nullable(DateTime('Asia/Shanghai'))", false, true, null) },
+
+ { typeof(string).FullName, CsToDb.New(DbType.String, "String", "String", false, null, "") },
+ { typeof(char).FullName, CsToDb.New(DbType.String, "String", "String", false, false, "") },{ typeof(char?).FullName, CsToDb.New(DbType.Single, "String","Nullable(String)", false, true, null) },
+ { typeof(Guid).FullName, CsToDb.New(DbType.String, "String", "String", false, false, Guid.Empty) },{ typeof(Guid?).FullName, CsToDb.New(DbType.String, "String", "Nullable(String)", false, true, null) },
+
+ };
+
+ public override DbInfoResult GetDbInfo(Type type)
+ {
+ if (_dicCsToDb.TryGetValue(type.FullName, out var trydc)) return new DbInfoResult((int)trydc.type, trydc.dbtype, trydc.dbtypeFull, trydc.isnullable, trydc.defaultValue);
+ if (type.IsArray) return null;
+ return null;
+ }
+
+ protected override string GetComparisonDDLStatements(params TypeAndName[] objects)
+ {
+ Object conn = null;
+ string database = null;
+
+ try
+ {
+ conn = _orm.Ado.MasterPool.Get(TimeSpan.FromSeconds(5));
+ database = conn.Value.Database;
+
+ var sb = new StringBuilder();
+ foreach (var obj in objects)
+ {
+ if (sb.Length > 0) sb.Append("\r\n");
+ var tb = _commonUtils.GetTableByEntity(obj.entityType);
+ if (tb == null) throw new Exception($"类型 {obj.entityType.FullName} 不可迁移");
+ if (tb.Columns.Any() == false) throw new Exception($"类型 {obj.entityType.FullName} 不可迁移,可迁移属性0个");
+ var tbname = _commonUtils.SplitTableName(tb.DbName);
+ if (tbname?.Length == 1) tbname = new[] { database, tbname[0] };
+
+ var tboldname = _commonUtils.SplitTableName(tb.DbOldName); //旧表名
+ if (tboldname?.Length == 1) tboldname = new[] { database, tboldname[0] };
+ if (string.IsNullOrEmpty(obj.tableName) == false)
+ {
+ var tbtmpname = _commonUtils.SplitTableName(obj.tableName);
+ if (tbtmpname?.Length == 1) tbtmpname = new[] { database, tbtmpname[0] };
+ if (tbname[0] != tbtmpname[0] || tbname[1] != tbtmpname[1])
+ {
+ tbname = tbtmpname;
+ tboldname = null;
+ }
+ }
+
+ if (string.Compare(tbname[0], database, true) != 0 && LocalExecuteScalar(database, _commonUtils.FormatSql(" select 1 from system.databases d where name={0}", tbname[0])) == null) //创建数据库
+ sb.Append($"CREATE DATABASE IF NOT EXISTS ").Append(_commonUtils.QuoteSqlName(tbname[0])).Append(" ENGINE=Ordinary;\r\n");
+
+ var sbalter = new StringBuilder();
+ var istmpatler = false; //创建临时表,导入数据,删除旧表,修改
+ if (LocalExecuteScalar(tbname[0], _commonUtils.FormatSql(" SELECT 1 FROM system.tables t WHERE database ={0} and name ={1}", tbname)) == null)
+ { //表不存在
+ if (tboldname != null)
+ {
+ if (string.Compare(tboldname[0], tbname[0], true) != 0 && LocalExecuteScalar(database, _commonUtils.FormatSql(" select 1 from system.databases where name={0}", tboldname[0])) == null ||
+ LocalExecuteScalar(tboldname[0], _commonUtils.FormatSql(" SELECT 1 FROM system.tables WHERE database={0} and name={1}", tboldname)) == null)
+ //数据库或表不存在
+ tboldname = null;
+ }
+ if (tboldname == null)
+ {
+ //创建表
+ var createTableName = _commonUtils.QuoteSqlName(tbname[0], tbname[1]);
+ sb.Append("CREATE TABLE IF NOT EXISTS ").Append(createTableName).Append(" ( ");
+ foreach (var tbcol in tb.ColumnsByPosition)
+ {
+ tbcol.Attribute.DbType = tbcol.Attribute.DbType.Replace(" NOT NULL", "");
+ sb.Append(" \r\n ").Append(_commonUtils.QuoteSqlName(tbcol.Attribute.Name)).Append(" ").Append(tbcol.Attribute.DbType);
+ if (string.IsNullOrEmpty(tbcol.Comment) == false) sb.Append(" COMMENT ").Append(_commonUtils.FormatSql("{0}", tbcol.Comment));
+ sb.Append(",");
+ }
+
+ foreach (var uk in tb.Indexes)
+ {
+ sb.Append(" \r\n ");
+ sb.Append("INDEX ").Append(_commonUtils.QuoteSqlName(ReplaceIndexName(uk.Name, tbname[1])));
+ foreach (var tbcol in uk.Columns)
+ {
+ sb.Append(" ");
+ sb.Append(_commonUtils.QuoteSqlName(tbcol.Column.Attribute.Name));
+ sb.Append("TYPE set(8192) GRANULARITY 5, ");
+ }
+ sb.Remove(sb.Length - 2, 2);
+ }
+ sb.Remove(sb.Length - 1, 1);
+ sb.Append("\r\n) ");
+ sb.Append("\r\nENGINE = MergeTree()");
+
+ if (tb.Primarys.Any())
+ {
+ sb.Append(" \r\nORDER BY ( ");
+ var ls = new StringBuilder();
+ foreach (var tbcol in tb.Primarys) ls.Append(_commonUtils.QuoteSqlName(tbcol.Attribute.Name)).Append(", ");
+ sb.Append(ls);
+ sb.Remove(sb.Length - 2, 2);
+ sb.Append(" )");
+ sb.Append(" \r\nPRIMARY KEY ");
+ sb.Append(ls);
+ sb.Remove(sb.Length - 2, 2).Append(",");
+ }
+ sb.Remove(sb.Length - 1, 1);
+ //if (string.IsNullOrEmpty(tb.Comment) == false)
+ // sb.Append(" Comment=").Append(_commonUtils.FormatSql("{0}", tb.Comment));
+ sb.Append(" SETTINGS index_granularity = 8192;\r\n");
+ continue;
+ }
+ //如果新表,旧表在一个数据库下,直接修改表名
+ if (string.Compare(tbname[0], tboldname[0], true) == 0)
+ sbalter.Append("ALTER TABLE ").Append(_commonUtils.QuoteSqlName(tboldname[0], tboldname[1])).Append(" RENAME TO ").Append(_commonUtils.QuoteSqlName(tbname[0], tbname[1])).Append(";\r\n");
+ else
+ {
+ //如果新表,旧表不在一起,创建新表,导入数据,删除旧表
+ istmpatler = true;
+ }
+ }
+ else
+ tboldname = null; //如果新表已经存在,不走改表名逻辑
+
+ //对比字段,只可以修改类型、增加字段、有限的修改字段名;保证安全不删除字段
+ var sql = _commonUtils.FormatSql(@"
+select
+a.name,
+a.type,
+if(ilike(a.`type`, 'Nullable(%S%)'),'is_nullable','0'),
+a.comment as comment,
+a.is_in_partition_key,
+a.is_in_sorting_key,
+a.is_in_primary_key,
+a.is_in_sampling_key
+from system.columns a
+where a.database in ({0}) and a.table in ({1})", tboldname ?? tbname);
+ var ds = _orm.Ado.ExecuteArray(CommandType.Text, sql);
+ var tbstruct = ds.ToDictionary(a => string.Concat(a[0]), a =>
+ {
+ return new
+ {
+ column = string.Concat(a[0]),
+ sqlType = (string)a[1],
+ is_nullable = string.Concat(a[2]) == "1",
+ is_identity = false,
+ comment = string.Concat(a[3]),
+ is_primary= string.Concat(a[6]) == "1",
+ };
+ }, StringComparer.CurrentCultureIgnoreCase);
+
+ if (istmpatler == false)
+ {
+ var existsPrimary = tbstruct.Any(o => o.Value.is_primary);
+ foreach (var tbcol in tb.ColumnsByPosition)
+ {
+ if (tbstruct.TryGetValue(tbcol.Attribute.Name, out var tbstructcol) ||
+ string.IsNullOrEmpty(tbcol.Attribute.OldName) == false && tbstruct.TryGetValue(tbcol.Attribute.OldName, out tbstructcol))
+ {
+ var isCommentChanged = tbstructcol.comment != (tbcol.Comment ?? "");
+ if (tbcol.Attribute.DbType.StartsWith(tbstructcol.sqlType, StringComparison.CurrentCultureIgnoreCase) == false ||
+ tbcol.Attribute.IsNullable != tbstructcol.is_nullable || isCommentChanged)
+ sbalter.Append("ALTER TABLE ").Append(_commonUtils.QuoteSqlName($"{tbname[0]}.{tbname[1]}")).Append(" MODIFY COLUMN ").Append(_commonUtils.QuoteSqlName(tbstructcol.column)).Append(tbcol.Attribute.IsNullable ? $"Nullable({tbcol.Attribute.DbType.Split(' ').First()})":tbcol.Attribute.DbType.Split(' ').First()).Append(";\r\n");
+ if(isCommentChanged) sbalter.Append("ALTER TABLE ").Append(_commonUtils.QuoteSqlName($"{tbname[0]}.{tbname[1]}")).Append(" COMMENT COLUMN ").Append(_commonUtils.QuoteSqlName(tbstructcol.column)).Append(_commonUtils.FormatSql("{0}", tbcol.Comment ?? "")).Append(";\r\n");
+ if (string.Compare(tbstructcol.column, tbcol.Attribute.OldName, true) == 0)
+ sbalter.Append("ALTER TABLE ").Append(_commonUtils.QuoteSqlName($"{tbname[0]}.{tbname[1]}")).Append(" COLUMN ").Append(_commonUtils.QuoteSqlName(tbstructcol.column)).Append(" TO ").Append(_commonUtils.QuoteSqlName(tbcol.Attribute.Name)).Append(";\r\n");
+ continue;
+ }
+ //添加列
+ sbalter.Append("ALTER TABLE ").Append(_commonUtils.QuoteSqlName(tbname[0], tbname[1])).Append(" ADD ").Append(_commonUtils.QuoteSqlName(tbcol.Attribute.Name)).Append(" ").Append(tbcol.Attribute.DbType);
+ if (tbcol.Attribute.IsNullable == false && tbcol.DbDefaultValue != "NULL" && tbcol.Attribute.IsIdentity == false) sbalter.Append(" DEFAULT ").Append(tbcol.DbDefaultValue);
+ if (string.IsNullOrEmpty(tbcol.Comment) == false) sbalter.Append(" COMMENT ").Append(_commonUtils.FormatSql("{0}", tbcol.Comment ?? ""));
+ sbalter.Append(";\r\n");
+ }
+
+ }
+
+
+ //创建临时表,数据导进临时表,然后删除原表,将临时表改名为原表名
+ var tablename = tboldname == null ? _commonUtils.QuoteSqlName(tbname[0], tbname[1]) : _commonUtils.QuoteSqlName(tboldname[0], tboldname[1]);
+ var tmptablename = _commonUtils.QuoteSqlName(tbname[0], $"FreeSqlTmp_{tbname[1]}");
+ //创建临时表
+ sb.Append("CREATE TABLE IF NOT EXISTS ").Append(tmptablename).Append(" ( ");
+ foreach (var tbcol in tb.ColumnsByPosition)
+ {
+ tbcol.Attribute.DbType = tbcol.Attribute.DbType.Replace(" NOT NULL", "");
+ sb.Append(" \r\n ").Append(_commonUtils.QuoteSqlName(tbcol.Attribute.Name)).Append(" ").Append(tbcol.Attribute.DbType);
+ if (string.IsNullOrEmpty(tbcol.Comment) == false) sb.Append(" COMMENT ").Append(_commonUtils.FormatSql("{0}", tbcol.Comment));
+ sb.Append(",");
+ }
+
+ foreach (var uk in tb.Indexes)
+ {
+ sb.Append(" \r\n ");
+ sb.Append("INDEX ").Append(_commonUtils.QuoteSqlName(ReplaceIndexName(uk.Name, tbname[1]))).Append("(");
+ foreach (var tbcol in uk.Columns)
+ {
+ sb.Append(_commonUtils.QuoteSqlName(tbcol.Column.Attribute.Name));
+ sb.Append("TYPE set(8192) GRANULARITY 5, ");
+ }
+ sb.Remove(sb.Length - 2, 2).Append("),");
+ }
+ sb.Remove(sb.Length - 1, 1);
+ sb.Append("\r\n) ");
+ sb.Append("\r\nENGINE = MergeTree()");
+
+ if (tb.Primarys.Any())
+ {
+ sb.Append(" \r\nORDER BY ( ");
+ var ls = new StringBuilder();
+ foreach (var tbcol in tb.Primarys) ls.Append(_commonUtils.QuoteSqlName(tbcol.Attribute.Name)).Append(", ");
+ sb.Append(ls);
+ sb.Remove(sb.Length - 2, 2);
+ sb.Append(" )");
+ sb.Append(" \r\nPRIMARY KEY ");
+ sb.Append(ls);
+ sb.Remove(sb.Length - 2, 2).Append(",");
+ }
+ sb.Remove(sb.Length - 1, 1);
+ //if (string.IsNullOrEmpty(tb.Comment) == false)
+ // sb.Append(" Comment=").Append(_commonUtils.FormatSql("{0}", tb.Comment));
+ sb.Append(" SETTINGS index_granularity = 8192;\r\n");
+
+ sb.Append("INSERT INTO ").Append(tmptablename).Append(" SELECT ");
+ foreach (var tbcol in tb.ColumnsByPosition)
+ {
+ var insertvalue = "NULL";
+ if (tbstruct.TryGetValue(tbcol.Attribute.Name, out var tbstructcol) ||
+ string.IsNullOrEmpty(tbcol.Attribute.OldName) == false && tbstruct.TryGetValue(tbcol.Attribute.OldName, out tbstructcol))
+ {
+ insertvalue = _commonUtils.QuoteSqlName(tbstructcol.column);
+ if (tbcol.Attribute.DbType.StartsWith(tbstructcol.sqlType, StringComparison.CurrentCultureIgnoreCase) == false)
+ {
+ //insertvalue = $"cast({insertvalue} as {tbcol.Attribute.DbType.Split(' ').First()})";
+ }
+ if (tbcol.Attribute.IsNullable != tbstructcol.is_nullable)
+ insertvalue = $"ifnull({insertvalue},{tbcol.DbDefaultValue})";
+ }
+ else if (tbcol.Attribute.IsNullable == false)
+ if (tbcol.DbDefaultValue != "NULL")
+ insertvalue = tbcol.DbDefaultValue;
+ sb.Append(insertvalue).Append(", ");
+ }
+ sb.Remove(sb.Length - 2, 2).Append(" FROM ").Append(tablename).Append(";\r\n");
+ sb.Append("DROP TABLE ").Append(tablename).Append(";\r\n");
+ sb.Append("RENAME TABLE ").Append(tmptablename).Append(" TO ").Append(_commonUtils.QuoteSqlName(tbname[0], tbname[1])).Append(";\r\n");
+ }
+ return sb.Length == 0 ? null : sb.ToString();
+ }
+ finally
+ {
+ try
+ {
+ if (string.IsNullOrEmpty(database) == false)
+ conn.Value.ChangeDatabase(database);
+ _orm.Ado.MasterPool.Return(conn);
+ }
+ catch
+ {
+ _orm.Ado.MasterPool.Return(conn, true);
+ }
+ }
+
+ object LocalExecuteScalar(string db, string sql)
+ {
+ if (string.Compare(database, db) != 0) conn.Value.ChangeDatabase(db);
+ try
+ {
+ using (var cmd = conn.Value.CreateCommand())
+ {
+ cmd.CommandText = sql;
+ cmd.CommandType = CommandType.Text;
+ return cmd.ExecuteScalar();
+ }
+ }
+ finally
+ {
+ if (string.Compare(database, db) != 0) conn.Value.ChangeDatabase(database);
+ }
+ }
+ }
+
+ public override int ExecuteDDLStatements(string ddl)
+ {
+ if (string.IsNullOrEmpty(ddl)) return 0;
+ var scripts = ddl.Split(new string[] { ";\r\n" }, StringSplitOptions.None).Where(a => string.IsNullOrEmpty(a.Trim()) == false).ToArray();
+
+ if (scripts.Any() == false) return 0;
+
+ var affrows = 0;
+ foreach (var script in scripts)
+ affrows += base.ExecuteDDLStatements(script);
+ return affrows;
+ }
+ }
+}
\ No newline at end of file
diff --git a/Providers/FreeSql.Provider.ClickHouse/ClickHouseDbFirst.cs b/Providers/FreeSql.Provider.ClickHouse/ClickHouseDbFirst.cs
new file mode 100644
index 00000000..92e57371
--- /dev/null
+++ b/Providers/FreeSql.Provider.ClickHouse/ClickHouseDbFirst.cs
@@ -0,0 +1,473 @@
+using FreeSql.DatabaseModel;
+using FreeSql.Internal;
+using FreeSql.Internal.Model;
+using System;
+using System.Collections.Generic;
+using System.Data;
+using System.Linq;
+using System.Text;
+using System.Text.RegularExpressions;
+using ClickHouse.Client.ADO;
+
+namespace FreeSql.ClickHouse
+{
+ class ClickHouseDbFirst : IDbFirst
+ {
+ IFreeSql _orm;
+ protected CommonUtils _commonUtils;
+ protected CommonExpression _commonExpression;
+ public ClickHouseDbFirst(IFreeSql orm, CommonUtils commonUtils, CommonExpression commonExpression)
+ {
+ _orm = orm;
+ _commonUtils = commonUtils;
+ _commonExpression = commonExpression;
+ }
+
+ public int GetDbType(DbColumnInfo column) => (int)GetClickHouseDbType(column);
+ DbType GetClickHouseDbType(DbColumnInfo column)
+ {
+ if (column.DbTypeText == "Nullable")
+ {
+ column.DbTypeText = column.DbTypeTextFull;
+ //(?<=\()(\S +)(?=\))
+ }
+ switch (column.DbTypeText.ToLower())
+ {
+ case "bit":
+ case "tinyint":
+ case "bool":
+ case "sbyte":
+ case "int8":
+ case "nullable(int8)": return DbType.SByte;
+ case "byte":
+ case "uint8":
+ case "nullable(uint8)": return DbType.Byte;
+ case "smallint":
+ case "int16":
+ case "nullable(int16)": return DbType.Int16;
+ case "uint16":
+ case "nullable(uint16)": return DbType.UInt16;
+ case "int32":
+ case "int":
+ case "nullable(int32)": return DbType.Int32;
+ case "uint":
+ case "uint32":
+ case "nullable(uint32)": return DbType.UInt32;
+ case "bigint":
+ case "int64":
+ case "long":
+ case "nullable(int64)": return DbType.Int64;
+ case "uint64":
+ case "ulong":
+ case "nullable(uint64)": return DbType.UInt64;
+ case "real":
+ case "Float64":
+ case "double":
+ case "nullable(float64)": return DbType.Double;
+ case "Float32":
+ case "float":
+ case "nullable(float32)": return DbType.Single;
+ case "decimal":
+ case "decimal128":
+ case "nullable(decimal128)": return DbType.Decimal;
+ case "date":
+ case "nullable(date)": return DbType.Date;
+ case "datetime":
+ case "nullable(datetime)": return DbType.DateTime;
+ case "datetime64":
+ case "nullable(datetime64)": return DbType.DateTime;
+ case "tinytext":
+ case "text":
+ case "mediumtext":
+ case "longtext":
+ case "char":
+ case "string":
+ case "nullable(string)":
+ case "varchar": return DbType.String;
+ default:
+ {
+ if (column.DbTypeText.ToLower().Contains("datetime"))
+ return DbType.DateTime;
+ return DbType.String;
+ }
+ }
+ }
+
+ static readonly Dictionary _dicDbToCs = new Dictionary() {
+ { (int)DbType.SByte, new DbToCs("(sbyte?)", "sbyte.Parse({0})", "{0}.ToString()", "sbyte?", typeof(sbyte), typeof(sbyte?), "{0}.Value", "GetByte") },
+ { (int)DbType.Int16, new DbToCs("(short?)", "short.Parse({0})", "{0}.ToString()", "short?", typeof(short), typeof(short?), "{0}.Value", "GetInt16") },
+ { (int)DbType.Int32, new DbToCs("(int?)", "int.Parse({0})", "{0}.ToString()", "int?", typeof(int), typeof(int?), "{0}.Value", "GetInt32") },
+ { (int)DbType.Int64, new DbToCs("(long?)", "long.Parse({0})", "{0}.ToString()", "long?", typeof(long), typeof(long?), "{0}.Value", "GetInt64") },
+
+ { (int)DbType.Byte, new DbToCs("(byte?)", "byte.Parse({0})", "{0}.ToString()", "byte?", typeof(byte), typeof(byte?), "{0}.Value", "GetByte") },
+ { (int)DbType.UInt16, new DbToCs("(ushort?)", "ushort.Parse({0})", "{0}.ToString()", "ushort?", typeof(ushort), typeof(ushort?), "{0}.Value", "GetInt16") },
+ { (int)DbType.UInt32, new DbToCs("(uint?)", "uint.Parse({0})", "{0}.ToString()", "uint?", typeof(uint), typeof(uint?), "{0}.Value", "GetInt32") },
+ { (int)DbType.UInt64, new DbToCs("(ulong?)", "ulong.Parse({0})", "{0}.ToString()", "ulong?", typeof(ulong), typeof(ulong?), "{0}.Value", "GetInt64") },
+
+ { (int)DbType.Double, new DbToCs("(double?)", "double.Parse({0})", "{0}.ToString()", "double?", typeof(double), typeof(double?), "{0}.Value", "GetDouble") },
+ { (int)DbType.Single, new DbToCs("(float?)", "float.Parse({0})", "{0}.ToString()", "float?", typeof(float), typeof(float?), "{0}.Value", "GetFloat") },
+ { (int)DbType.Decimal, new DbToCs("(decimal?)", "decimal.Parse({0})", "{0}.ToString()", "decimal?", typeof(decimal), typeof(decimal?), "{0}.Value", "GetDecimal") },
+
+ { (int)DbType.Date, new DbToCs("(DateTime?)", "new DateTime(long.Parse({0}))", "{0}.Ticks.ToString()", "DateTime?", typeof(DateTime), typeof(DateTime?), "{0}.Value", "GetDateTime") },
+ { (int)DbType.Date, new DbToCs("(DateTime?)", "new DateTime(long.Parse({0}))", "{0}.Ticks.ToString()", "DateTime?", typeof(DateTime), typeof(DateTime?), "{0}.Value", "GetDateTime") },
+ { (int)DbType.DateTime, new DbToCs("(DateTime?)", "new DateTime(long.Parse({0}))", "{0}.Ticks.ToString()", "DateTime?", typeof(DateTime), typeof(DateTime?), "{0}.Value", "GetDateTime") },
+ { (int)DbType.DateTime2, new DbToCs("(DateTime?)", "new DateTime(long.Parse({0}))", "{0}.Ticks.ToString()", "DateTime?", typeof(DateTime), typeof(DateTime?), "{0}.Value", "GetDateTime") },
+
+ { (int)DbType.Guid, new DbToCs("(Guid?)", "Guid.Parse({0})", "{0}.ToString()", "Guid?", typeof(Guid), typeof(Guid?), "{0}", "GetString") },
+ { (int)DbType.String, new DbToCs("", "{0}.Replace(StringifySplit, \"|\")", "{0}.Replace(\"|\", StringifySplit)", "string", typeof(string), typeof(string), "{0}", "GetString") },
+
+ };
+
+ public string GetCsConvert(DbColumnInfo column) => _dicDbToCs.TryGetValue(column.DbType, out var trydc) ? (column.IsNullable ? trydc.csConvert : trydc.csConvert.Replace("?", "")) : null;
+ public string GetCsParse(DbColumnInfo column) => _dicDbToCs.TryGetValue(column.DbType, out var trydc) ? trydc.csParse : null;
+ public string GetCsStringify(DbColumnInfo column) => _dicDbToCs.TryGetValue(column.DbType, out var trydc) ? trydc.csStringify : null;
+ public string GetCsType(DbColumnInfo column) => _dicDbToCs.TryGetValue(column.DbType, out var trydc) ? (column.IsNullable ? trydc.csType : trydc.csType.Replace("?", "")) : null;
+ public Type GetCsTypeInfo(DbColumnInfo column) => _dicDbToCs.TryGetValue(column.DbType, out var trydc) ? trydc.csTypeInfo : null;
+ public string GetCsTypeValue(DbColumnInfo column) => _dicDbToCs.TryGetValue(column.DbType, out var trydc) ? trydc.csTypeValue : null;
+ public string GetDataReaderMethod(DbColumnInfo column) => _dicDbToCs.TryGetValue(column.DbType, out var trydc) ? trydc.dataReaderMethod : null;
+
+ public List GetDatabases()
+ {
+ var sql = @" select name from system.databases where name not in ('system', 'default')";
+ var ds = _orm.Ado.ExecuteArray(CommandType.Text, sql);
+ return ds.Select(a => a.FirstOrDefault()?.ToString()).ToList();
+ }
+
+ public bool ExistsTable(string name, bool ignoreCase)
+ {
+ if (string.IsNullOrEmpty(name)) return false;
+ var tbname = _commonUtils.SplitTableName(name);
+ if (tbname?.Length == 1)
+ {
+ var database = "";
+ using (var conn = _orm.Ado.MasterPool.Get(TimeSpan.FromSeconds(5)))
+ {
+ database = conn.Value.Database;
+ }
+ tbname = new[] { database, tbname[0] };
+ }
+ if (ignoreCase) tbname = tbname.Select(a => a.ToLower()).ToArray();
+ var sql = $" SELECT 1 FROM information_schema.TABLES WHERE {(ignoreCase ? "lower(table_schema)" : "table_schema")} = {_commonUtils.FormatSql("{0}", tbname[0])} and {(ignoreCase ? "lower(table_name)" : "table_name")} = {_commonUtils.FormatSql("{0}", tbname[1])}";
+ return string.Concat(_orm.Ado.ExecuteScalar(CommandType.Text, sql)) == "1";
+ }
+
+ public DbTableInfo GetTableByName(string name, bool ignoreCase = true) => GetTables(null, name, ignoreCase)?.FirstOrDefault();
+ public List GetTablesByDatabase(params string[] database) => GetTables(database, null, false);
+
+ public List GetTables(string[] database, string tablename, bool ignoreCase)
+ {
+ var loc1 = new List();
+ var loc2 = new Dictionary(StringComparer.CurrentCultureIgnoreCase);
+ var loc3 = new Dictionary>(StringComparer.CurrentCultureIgnoreCase);
+ string[] tbname = null;
+ if (string.IsNullOrEmpty(tablename) == false)
+ {
+ tbname = _commonUtils.SplitTableName(tablename);
+ if (tbname?.Length == 1)
+ {
+ using (var conn = _orm.Ado.MasterPool.Get(TimeSpan.FromSeconds(5)))
+ {
+ if (string.IsNullOrEmpty(conn.Value.Database)) return loc1;
+ tbname = new[] { conn.Value.Database, tbname[0] };
+ }
+ }
+ if (ignoreCase) tbname = tbname.Select(a => a.ToLower()).ToArray();
+ database = new[] { tbname[0] };
+ }
+ else if (database == null || database.Any() == false)
+ {
+ using (var conn = _orm.Ado.MasterPool.Get())
+ {
+ if (string.IsNullOrEmpty(conn.Value.Database)) return loc1;
+ database = new[] { conn.Value.Database };
+ }
+ }
+
+ var databaseIn = string.Join(",", database.Select(a => _commonUtils.FormatSql("{0}", a)));
+ var sql = $@"
+select
+concat(a.table_schema, '.', a.table_name) 'id',
+a.table_schema 'schema',
+a.table_name 'table',
+a.table_comment,
+a.table_type 'type'
+from information_schema.tables a
+where {(ignoreCase ? "lower(a.table_schema)" : "a.table_schema")} in ({databaseIn}){(tbname == null ? "" : $" and {(ignoreCase ? "lower(a.table_name)" : "a.table_name")}={_commonUtils.FormatSql("{0}", tbname[1])}")}";
+ var ds = _orm.Ado.ExecuteArray(CommandType.Text, sql);
+ if (ds == null) return loc1;
+
+ var loc6 = new List();
+ var loc66 = new List();
+ var loc6_1000 = new List();
+ var loc66_1000 = new List();
+ foreach (var row in ds)
+ {
+ var table_id = string.Concat(row[0]);
+ var schema = string.Concat(row[1]);
+ var table = string.Concat(row[2]);
+ var comment = string.Concat(row[3]);
+ var type = string.Concat(row[4]) == "VIEW" ? DbTableType.VIEW : DbTableType.TABLE;
+ if (database.Length == 1)
+ {
+ table_id = table_id.Substring(table_id.IndexOf('.') + 1);
+ schema = "";
+ }
+ loc2.Add(table_id, new DbTableInfo { Id = table_id, Schema = schema, Name = table, Comment = comment, Type = type });
+ loc3.Add(table_id, new Dictionary(StringComparer.CurrentCultureIgnoreCase));
+ switch (type)
+ {
+ case DbTableType.TABLE:
+ case DbTableType.VIEW:
+ loc6_1000.Add(table.Replace("'", "''"));
+ if (loc6_1000.Count >= 500)
+ {
+ loc6.Add(loc6_1000.ToArray());
+ loc6_1000.Clear();
+ }
+ break;
+ case DbTableType.StoreProcedure:
+ loc66_1000.Add(table.Replace("'", "''"));
+ if (loc66_1000.Count >= 500)
+ {
+ loc66.Add(loc66_1000.ToArray());
+ loc66_1000.Clear();
+ }
+ break;
+ }
+ }
+ if (loc6_1000.Count > 0) loc6.Add(loc6_1000.ToArray());
+ if (loc66_1000.Count > 0) loc66.Add(loc66_1000.ToArray());
+
+ if (loc6.Count == 0) return loc1;
+ var loc8 = new StringBuilder().Append("(");
+ for (var loc8idx = 0; loc8idx < loc6.Count; loc8idx++)
+ {
+ if (loc8idx > 0) loc8.Append(" OR ");
+ loc8.Append("a.table_name in (");
+ for (var loc8idx2 = 0; loc8idx2 < loc6[loc8idx].Length; loc8idx2++)
+ {
+ if (loc8idx2 > 0) loc8.Append(",");
+ loc8.Append($"'{loc6[loc8idx][loc8idx2]}'");
+ }
+ loc8.Append(")");
+ }
+ loc8.Append(")");
+
+ sql = $@"
+select
+concat(a.table_schema, '.', a.table_name),
+a.column_name,
+a.data_type,
+ifnull(a.character_maximum_length, 0) 'len',
+a.column_type,
+case when a.is_nullable = 'YES' then 1 else 0 end 'is_nullable',
+case when locate('auto_increment', a.extra) > 0 then 1 else 0 end 'is_identity',
+a.column_comment 'comment',
+a.column_default 'default_value'
+from information_schema.columns a
+where {(ignoreCase ? "lower(a.table_schema)" : "a.table_schema")} in ({databaseIn}) and {loc8}
+";
+ ds = _orm.Ado.ExecuteArray(CommandType.Text, sql);
+ if (ds == null) return loc1;
+
+ var position = 0;
+ foreach (var row in ds)
+ {
+ string table_id = string.Concat(row[0]);
+ string column = string.Concat(row[1]);
+ string type = string.Concat(row[2]);
+ //long max_length = long.Parse(string.Concat(row[3]));
+ string sqlType = string.Concat(row[4]);
+ var m_len = Regex.Match(sqlType, @"\w+\((\d+)");
+ int max_length = m_len.Success ? int.Parse(m_len.Groups[1].Value) : -1;
+ bool is_nullable = string.Concat(row[5]) == "1";
+ bool is_identity = string.Concat(row[6]) == "1";
+ string comment = string.Concat(row[7]);
+ string defaultValue = string.Concat(row[8]);
+ if (max_length == 0) max_length = -1;
+ if (database.Length == 1)
+ {
+ table_id = table_id.Substring(table_id.IndexOf('.') + 1);
+ }
+ loc3[table_id].Add(column, new DbColumnInfo
+ {
+ Name = column,
+ MaxLength = max_length,
+ IsIdentity = is_identity,
+ IsNullable = is_nullable,
+ IsPrimary = false,
+ DbTypeText = type,
+ DbTypeTextFull = sqlType,
+ Table = loc2[table_id],
+ Coment = comment,
+ DefaultValue = defaultValue,
+ Position = ++position
+ });
+ loc3[table_id][column].DbType = this.GetDbType(loc3[table_id][column]);
+ loc3[table_id][column].CsType = this.GetCsTypeInfo(loc3[table_id][column]);
+ }
+
+ sql = $@"
+select
+concat(a.table_schema, '.', a.table_name) 'table_id',
+a.column_name,
+a.index_name 'index_id',
+case when a.non_unique = 0 then 1 else 0 end 'IsUnique',
+case when a.index_name = 'PRIMARY' then 1 else 0 end 'IsPrimaryKey',
+0 'IsClustered',
+0 'IsDesc'
+from information_schema.statistics a
+where {(ignoreCase ? "lower(a.table_schema)" : "a.table_schema")} in ({databaseIn}) and {loc8}
+";
+ ds = _orm.Ado.ExecuteArray(CommandType.Text, sql);
+ if (ds == null) return loc1;
+
+ var indexColumns = new Dictionary>(StringComparer.CurrentCultureIgnoreCase);
+ var uniqueColumns = new Dictionary>(StringComparer.CurrentCultureIgnoreCase);
+ foreach (var row in ds)
+ {
+ string table_id = string.Concat(row[0]);
+ string column = string.Concat(row[1]);
+ string index_id = string.Concat(row[2]);
+ bool is_unique = string.Concat(row[3]) == "1";
+ bool is_primary_key = string.Concat(row[4]) == "1";
+ bool is_clustered = string.Concat(row[5]) == "1";
+ bool is_desc = string.Concat(row[6]) == "1";
+ if (database.Length == 1)
+ table_id = table_id.Substring(table_id.IndexOf('.') + 1);
+ if (loc3.ContainsKey(table_id) == false || loc3[table_id].ContainsKey(column) == false) continue;
+ var loc9 = loc3[table_id][column];
+ if (loc9.IsPrimary == false && is_primary_key) loc9.IsPrimary = is_primary_key;
+
+ Dictionary loc10 = null;
+ DbIndexInfo loc11 = null;
+ if (!indexColumns.TryGetValue(table_id, out loc10))
+ indexColumns.Add(table_id, loc10 = new Dictionary(StringComparer.CurrentCultureIgnoreCase));
+ if (!loc10.TryGetValue(index_id, out loc11))
+ loc10.Add(index_id, loc11 = new DbIndexInfo());
+ loc11.Columns.Add(new DbIndexColumnInfo { Column = loc9, IsDesc = is_desc });
+ if (is_unique && !is_primary_key)
+ {
+ if (!uniqueColumns.TryGetValue(table_id, out loc10))
+ uniqueColumns.Add(table_id, loc10 = new Dictionary(StringComparer.CurrentCultureIgnoreCase));
+ if (!loc10.TryGetValue(index_id, out loc11))
+ loc10.Add(index_id, loc11 = new DbIndexInfo());
+ loc11.Columns.Add(new DbIndexColumnInfo { Column = loc9, IsDesc = is_desc });
+ }
+ }
+ foreach (string table_id in indexColumns.Keys)
+ {
+ foreach (var column in indexColumns[table_id])
+ loc2[table_id].IndexesDict.Add(column.Key, column.Value);
+ }
+ foreach (string table_id in uniqueColumns.Keys)
+ {
+ foreach (var column in uniqueColumns[table_id])
+ {
+ column.Value.Columns.Sort((c1, c2) => c1.Column.Name.CompareTo(c2.Column.Name));
+ loc2[table_id].UniquesDict.Add(column.Key, column.Value);
+ }
+ }
+
+ if (tbname == null)
+ {
+ sql = $@"
+select
+concat(a.constraint_schema, '.', a.table_name) 'table_id',
+a.column_name,
+a.constraint_name 'FKId',
+concat(a.referenced_table_schema, '.', a.referenced_table_name) 'ref_table_id',
+1 'IsForeignKey',
+a.referenced_column_name 'ref_column'
+from information_schema.key_column_usage a
+where {(ignoreCase ? "lower(a.constraint_schema)" : "a.constraint_schema")} in ({databaseIn}) and {loc8} and not isnull(position_in_unique_constraint)
+";
+ ds = _orm.Ado.ExecuteArray(CommandType.Text, sql);
+ if (ds == null) return loc1;
+
+ var fkColumns = new Dictionary>(StringComparer.CurrentCultureIgnoreCase);
+ foreach (var row in ds)
+ {
+ string table_id = string.Concat(row[0]);
+ string column = string.Concat(row[1]);
+ string fk_id = string.Concat(row[2]);
+ string ref_table_id = string.Concat(row[3]);
+ bool is_foreign_key = string.Concat(row[4]) == "1";
+ string referenced_column = string.Concat(row[5]);
+ if (database.Length == 1)
+ {
+ table_id = table_id.Substring(table_id.IndexOf('.') + 1);
+ ref_table_id = ref_table_id.Substring(ref_table_id.IndexOf('.') + 1);
+ }
+ if (loc3.ContainsKey(table_id) == false || loc3[table_id].ContainsKey(column) == false) continue;
+ var loc9 = loc3[table_id][column];
+ if (loc2.ContainsKey(ref_table_id) == false) continue;
+ var loc10 = loc2[ref_table_id];
+ var loc11 = loc3[ref_table_id][referenced_column];
+
+ Dictionary loc12 = null;
+ DbForeignInfo loc13 = null;
+ if (!fkColumns.TryGetValue(table_id, out loc12))
+ fkColumns.Add(table_id, loc12 = new Dictionary(StringComparer.CurrentCultureIgnoreCase));
+ if (!loc12.TryGetValue(fk_id, out loc13))
+ loc12.Add(fk_id, loc13 = new DbForeignInfo { Table = loc2[table_id], ReferencedTable = loc10 });
+ loc13.Columns.Add(loc9);
+ loc13.ReferencedColumns.Add(loc11);
+ }
+ foreach (var table_id in fkColumns.Keys)
+ foreach (var fk in fkColumns[table_id])
+ loc2[table_id].ForeignsDict.Add(fk.Key, fk.Value);
+ }
+
+ foreach (var table_id in loc3.Keys)
+ {
+ foreach (var loc5 in loc3[table_id].Values)
+ {
+ loc2[table_id].Columns.Add(loc5);
+ if (loc5.IsIdentity) loc2[table_id].Identitys.Add(loc5);
+ if (loc5.IsPrimary) loc2[table_id].Primarys.Add(loc5);
+ }
+ }
+ foreach (var loc4 in loc2.Values)
+ {
+ //if (loc4.Primarys.Count == 0 && loc4.UniquesDict.Count > 0)
+ //{
+ // foreach (var loc5 in loc4.UniquesDict.First().Value.Columns)
+ // {
+ // loc5.Column.IsPrimary = true;
+ // loc4.Primarys.Add(loc5.Column);
+ // }
+ //}
+ loc4.Primarys.Sort((c1, c2) => c1.Name.CompareTo(c2.Name));
+ loc4.Columns.Sort((c1, c2) =>
+ {
+ int compare = c2.IsPrimary.CompareTo(c1.IsPrimary);
+ if (compare == 0)
+ {
+ bool b1 = loc4.ForeignsDict.Values.Where(fk => fk.Columns.Where(c3 => c3.Name == c1.Name).Any()).Any();
+ bool b2 = loc4.ForeignsDict.Values.Where(fk => fk.Columns.Where(c3 => c3.Name == c2.Name).Any()).Any();
+ compare = b2.CompareTo(b1);
+ }
+ if (compare == 0) compare = c1.Name.CompareTo(c2.Name);
+ return compare;
+ });
+ loc1.Add(loc4);
+ }
+ loc1.Sort((t1, t2) =>
+ {
+ var ret = t1.Schema.CompareTo(t2.Schema);
+ if (ret == 0) ret = t1.Name.CompareTo(t2.Name);
+ return ret;
+ });
+
+ loc2.Clear();
+ loc3.Clear();
+ return loc1;
+ }
+
+ public List GetEnumsByDatabase(params string[] database)
+ {
+ return new List();
+ }
+ }
+}
\ No newline at end of file
diff --git a/Providers/FreeSql.Provider.ClickHouse/ClickHouseExpression.cs b/Providers/FreeSql.Provider.ClickHouse/ClickHouseExpression.cs
new file mode 100644
index 00000000..dc5ab194
--- /dev/null
+++ b/Providers/FreeSql.Provider.ClickHouse/ClickHouseExpression.cs
@@ -0,0 +1,575 @@
+using FreeSql.Internal;
+using FreeSql.Internal.Model;
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Linq;
+using System.Linq.Expressions;
+using System.Text;
+using System.Text.RegularExpressions;
+
+namespace FreeSql.ClickHouse
+{
+ class ClickHouseExpression : CommonExpression
+ {
+
+ public ClickHouseExpression(CommonUtils common) : base(common) { }
+
+ public override string ExpressionLambdaToSqlOther(Expression exp, ExpTSC tsc)
+ {
+ Func getExp = exparg => ExpressionLambdaToSql(exparg, tsc);
+ switch (exp.NodeType)
+ {
+ case ExpressionType.ArrayLength:
+ var arrOper = (exp as UnaryExpression)?.Operand;
+ if (arrOper.Type == typeof(byte[])) return $"length({getExp(arrOper)})";
+ break;
+ case ExpressionType.Convert:
+ var operandExp = (exp as UnaryExpression)?.Operand;
+ var gentype = exp.Type.NullableTypeOrThis();
+ if (gentype != operandExp.Type.NullableTypeOrThis())
+ {
+ switch (gentype.ToString())
+ {
+ case "System.Boolean": return $"({getExp(operandExp)} not in ('0','false'))";
+ case "System.Byte": return $"cast({getExp(operandExp)} as Int8)";
+ case "System.Char": return $"substr(cast({getExp(operandExp)} as String), 1, 1)";
+ case "System.DateTime": return $"cast({getExp(operandExp)} as DateTime)";
+ case "System.Decimal": return $"cast({getExp(operandExp)} as Decimal128(19))";
+ case "System.Double": return $"cast({getExp(operandExp)} as Float64)";
+ case "System.Int16": return $"cast({getExp(operandExp)} as Int16)";
+ case "System.Int32": return $"cast({getExp(operandExp)} as Int32)";
+ case "System.Int64": return $"cast({getExp(operandExp)} as Int64)";
+ case "System.SByte": return $"cast({getExp(operandExp)} as UInt8)";
+ case "System.Single": return $"cast({getExp(operandExp)} as Float32)";
+ case "System.String": return $"cast({getExp(operandExp)} as String)";
+ case "System.UInt16": return $"cast({getExp(operandExp)} as UInt16)";
+ case "System.UInt32": return $"cast({getExp(operandExp)} as UInt32)";
+ case "System.UInt64": return $"cast({getExp(operandExp)} as UInt64)";
+ case "System.Guid": return $"substr(cast({getExp(operandExp)} as String), 1, 36)";
+ }
+ }
+ break;
+ case ExpressionType.Call:
+ var callExp = exp as MethodCallExpression;
+
+ switch (callExp.Method.Name)
+ {
+ case "Parse":
+ case "TryParse":
+ switch (callExp.Method.DeclaringType.NullableTypeOrThis().ToString())
+ {
+ case "System.Boolean": return $"({getExp(callExp.Arguments[0])} not in ('0','false'))";
+ case "System.Byte": return $"cast({getExp(callExp.Arguments[0])} as Int8)";
+ case "System.Char": return $"substr(cast({getExp(callExp.Arguments[0])} as String), 1, 1)";
+ case "System.DateTime": return $"cast({getExp(callExp.Arguments[0])} as DateTime)";
+ case "System.Decimal": return $"cast({getExp(callExp.Arguments[0])} as Decimal128(19))";
+ case "System.Double": return $"cast({getExp(callExp.Arguments[0])} as Float64)";
+ case "System.Int16": return $"cast({getExp(callExp.Arguments[0])} as Int16)";
+ case "System.Int32": return $"cast({getExp(callExp.Arguments[0])} as Int32)";
+ case "System.Int64": return $"cast({getExp(callExp.Arguments[0])} as Int64)";
+ case "System.SByte": return $"cast({getExp(callExp.Arguments[0])} as UInt8)";
+ case "System.Single": return $"cast({getExp(callExp.Arguments[0])} as Float32)";
+ case "System.UInt16": return $"cast({getExp(callExp.Arguments[0])} as UInt16)";
+ case "System.UInt32": return $"cast({getExp(callExp.Arguments[0])} as UInt32)";
+ case "System.UInt64": return $"cast({getExp(callExp.Arguments[0])} as UInt64)";
+ case "System.Guid": return $"substr(cast({getExp(callExp.Arguments[0])} as String), 1, 36)";
+ }
+ return null;
+ case "NewGuid":
+ return null;
+ case "Next":
+ if (callExp.Object?.Type == typeof(Random)) return "cast(rand()*1000000000 as Int64)";
+ return null;
+ case "NextDouble":
+ if (callExp.Object?.Type == typeof(Random)) return "rand()";
+ return null;
+ case "Random":
+ if (callExp.Method.DeclaringType.IsNumberType()) return "rand()";
+ return null;
+ case "ToString":
+ if (callExp.Object != null) return callExp.Arguments.Count == 0 ? $"cast({getExp(callExp.Object)} as String)" : null;
+ return null;
+ }
+
+ var objExp = callExp.Object;
+ var objType = objExp?.Type;
+ if (objType?.FullName == "System.Byte[]") return null;
+
+ var argIndex = 0;
+ if (objType == null && callExp.Method.DeclaringType == typeof(Enumerable))
+ {
+ objExp = callExp.Arguments.FirstOrDefault();
+ objType = objExp?.Type;
+ argIndex++;
+
+ if (objType == typeof(string))
+ {
+ switch (callExp.Method.Name)
+ {
+ case "First":
+ case "FirstOrDefault":
+ return $"substr({getExp(callExp.Arguments[0])}, 1, 1)";
+ }
+ }
+ }
+ if (objType == null) objType = callExp.Method.DeclaringType;
+ if (objType != null || objType.IsArrayOrList())
+ {
+ if (argIndex >= callExp.Arguments.Count) break;
+ tsc.SetMapColumnTmp(null);
+ var args1 = getExp(callExp.Arguments[argIndex]);
+ var oldMapType = tsc.SetMapTypeReturnOld(tsc.mapTypeTmp);
+ var oldDbParams = tsc.SetDbParamsReturnOld(null);
+ var left = objExp == null ? null : getExp(objExp);
+ tsc.SetMapColumnTmp(null).SetMapTypeReturnOld(oldMapType);
+ tsc.SetDbParamsReturnOld(oldDbParams);
+ switch (callExp.Method.Name)
+ {
+ case "Contains":
+ //判断 in //在各大 Provider AdoProvider 中已约定,500元素分割, 3空格\r\n4空格
+ return $"(({args1}) in {left.Replace(", \r\n \r\n", $") \r\n OR ({args1}) in (")})";
+ }
+ }
+ break;
+ case ExpressionType.NewArrayInit:
+ var arrExp = exp as NewArrayExpression;
+ var arrSb = new StringBuilder();
+ arrSb.Append("(");
+ for (var a = 0; a < arrExp.Expressions.Count; a++)
+ {
+ if (a > 0) arrSb.Append(",");
+ if (a % 500 == 499) arrSb.Append(" \r\n \r\n"); //500元素分割, 3空格\r\n4空格
+ arrSb.Append(getExp(arrExp.Expressions[a]));
+ }
+ if (arrSb.Length == 1) arrSb.Append("NULL");
+ return arrSb.Append(")").ToString();
+ case ExpressionType.ListInit:
+ var listExp = exp as ListInitExpression;
+ var listSb = new StringBuilder();
+ listSb.Append("(");
+ for (var a = 0; a < listExp.Initializers.Count; a++)
+ {
+ if (listExp.Initializers[a].Arguments.Any() == false) continue;
+ if (a > 0) listSb.Append(",");
+ listSb.Append(getExp(listExp.Initializers[a].Arguments.FirstOrDefault()));
+ }
+ if (listSb.Length == 1) listSb.Append("NULL");
+ return listSb.Append(")").ToString();
+ case ExpressionType.New:
+ var newExp = exp as NewExpression;
+ if (typeof(IList).IsAssignableFrom(newExp.Type))
+ {
+ if (newExp.Arguments.Count == 0) return "(NULL)";
+ if (typeof(IEnumerable).IsAssignableFrom(newExp.Arguments[0].Type) == false) return "(NULL)";
+ return getExp(newExp.Arguments[0]);
+ }
+ return null;
+ }
+ return null;
+ }
+
+ public override string ExpressionLambdaToSqlMemberAccessString(MemberExpression exp, ExpTSC tsc)
+ {
+ if (exp.Expression == null)
+ {
+ switch (exp.Member.Name)
+ {
+ case "Empty": return "''";
+ }
+ return null;
+ }
+ var left = ExpressionLambdaToSql(exp.Expression, tsc);
+ switch (exp.Member.Name)
+ {
+ case "Length": return $"char_length({left})";
+ }
+ return null;
+ }
+ public override string ExpressionLambdaToSqlMemberAccessDateTime(MemberExpression exp, ExpTSC tsc)
+ {
+ if (exp.Expression == null)
+ {
+ switch (exp.Member.Name)
+ {
+ case "Now": return _common.Now;
+ case "UtcNow": return _common.NowUtc;
+ case "Today": return "curdate()";
+ case "MinValue": return "cast('0001/1/1 0:00:00' as DateTime)";
+ case "MaxValue": return "cast('9999/12/31 23:59:59' as DateTime)";
+ }
+ return null;
+ }
+ var left = ExpressionLambdaToSql(exp.Expression, tsc);
+ if ((exp.Expression as MemberExpression)?.Expression.NodeType == ExpressionType.Constant)
+ left = $"toDateTime({left})";
+
+ //IsDate(left);
+ switch (exp.Member.Name)
+ {
+ case "Date": return $"toDate({left})";
+ case "TimeOfDay": return $"dateDiff(second, toDate({left}), toDateTime({left}))*1000000";
+ case "DayOfWeek": return $"(toDayOfWeek({left})-1)";
+ case "Day": return $"toDayOfMonth({left})";
+ case "DayOfYear": return $"toDayOfYear({left})";
+ case "Month": return $"toMonth({left})";
+ case "Year": return $"toYear({left})";
+ case "Hour": return $"toHour({left})";
+ case "Minute": return $"toMinute({left})";
+ case "Second": return $"toSecond({left})";
+ case "Millisecond": return $"(toSecond({left})/1000)";
+ case "Ticks": return $"(dateDiff(second, toDate('0001-1-1'), toDateTime({left}))*10000000+621355968000000000)";
+ }
+ return null;
+ }
+ public bool IsInt(string _string)
+ {
+ if (string.IsNullOrEmpty(_string))
+ return false;
+ int i = 0;
+ return int.TryParse(_string, out i);
+ }
+ public bool IsDate(string date)
+ {
+ if (string.IsNullOrEmpty(date))
+ return true;
+ return DateTime.TryParse(date, out var time);
+ }
+ public override string ExpressionLambdaToSqlMemberAccessTimeSpan(MemberExpression exp, ExpTSC tsc)
+ {
+ if (exp.Expression == null)
+ {
+ switch (exp.Member.Name)
+ {
+ case "Zero": return "0";
+ case "MinValue": return "-922337203685477580"; //微秒 Ticks / 10
+ case "MaxValue": return "922337203685477580";
+ }
+ return null;
+ }
+ var left = ExpressionLambdaToSql(exp.Expression, tsc);
+ switch (exp.Member.Name)
+ {
+ case "Days": return $"intDiv(({left})/{60 * 60 * 24})";
+ case "Hours": return $"intDiv(({left})/{60 * 60}%24)";
+ case "Milliseconds": return $"(cast({left} as Int64)*1000)";
+ case "Minutes": return $"intDiv(({left})/60%60)";
+ case "Seconds": return $"(({left})%60)";
+ case "Ticks": return $"(intDiv({left} as Int64)*10000000)";
+ case "TotalDays": return $"(({left})/{60 * 60 * 24})";
+ case "TotalHours": return $"(({left})/{60 * 60})";
+ case "TotalMilliseconds": return $"(cast({left} as Int64)*1000)";
+ case "TotalMinutes": return $"(({left})/60)";
+ case "TotalSeconds": return $"({left})";
+ }
+ return null;
+ }
+
+ public override string ExpressionLambdaToSqlCallString(MethodCallExpression exp, ExpTSC tsc)
+ {
+ Func getExp = exparg => ExpressionLambdaToSql(exparg, tsc);
+ if (exp.Object == null)
+ {
+ switch (exp.Method.Name)
+ {
+ case "IsNullOrEmpty":
+ var arg1 = getExp(exp.Arguments[0]);
+ return $"({arg1} is null or {arg1} = '')";
+ case "IsNullOrWhiteSpace":
+ var arg2 = getExp(exp.Arguments[0]);
+ return $"({arg2} is null or {arg2} = '' or ltrim({arg2}) = '')";
+ case "Concat":
+ return _common.StringConcat(exp.Arguments.Select(a => getExp(a)).ToArray(), null);
+ case "Format":
+ if (exp.Arguments[0].NodeType != ExpressionType.Constant) throw new Exception($"未实现函数表达式 {exp} 解析,参数 {exp.Arguments[0]} 必须为常量");
+ if (exp.Arguments.Count == 1) return ExpressionLambdaToSql(exp.Arguments[0], tsc);
+ var expArgsHack = exp.Arguments.Count == 2 && exp.Arguments[1].NodeType == ExpressionType.NewArrayInit ?
+ (exp.Arguments[1] as NewArrayExpression).Expressions : exp.Arguments.Where((a, z) => z > 0);
+ //3个 {} 时,Arguments 解析出来是分开的
+ //4个 {} 时,Arguments[1] 只能解析这个出来,然后里面是 NewArray []
+ var expArgs = expArgsHack.Select(a => $"',{_common.IsNull(ExpressionLambdaToSql(a, tsc), "''")},'").ToArray();
+ return $"concat({string.Format(ExpressionLambdaToSql(exp.Arguments[0], tsc), expArgs)})";
+ case "Join":
+ if (exp.IsStringJoin(out var tolistObjectExp, out var toListMethod, out var toListArgs1))
+ {
+ var newToListArgs0 = Expression.Call(tolistObjectExp, toListMethod,
+ Expression.Lambda(
+ Expression.Call(
+ typeof(SqlExtExtensions).GetMethod("StringJoinClickHouseGroupConcat"),
+ Expression.Convert(toListArgs1.Body, typeof(object)),
+ Expression.Convert(exp.Arguments[0], typeof(object))),
+ toListArgs1.Parameters));
+ var newToListSql = getExp(newToListArgs0);
+ return newToListSql;
+ }
+ break;
+ }
+ }
+ else
+ {
+ var left = getExp(exp.Object);
+ switch (exp.Method.Name)
+ {
+ case "StartsWith":
+ case "EndsWith":
+ case "Contains":
+ var args0Value = getExp(exp.Arguments[0]);
+ if (args0Value == "NULL") return $"({left}) IS NULL";
+ if (exp.Method.Name == "StartsWith") return $"({left}) LIKE {(args0Value.EndsWith("'") ? args0Value.Insert(args0Value.Length - 1, "%") : $"concat({args0Value}, '%')")}";
+ if (exp.Method.Name == "EndsWith") return $"({left}) LIKE {(args0Value.StartsWith("'") ? args0Value.Insert(1, "%") : $"concat('%', {args0Value})")}";
+ if (args0Value.StartsWith("'") && args0Value.EndsWith("'")) return $"({left}) LIKE {args0Value.Insert(1, "%").Insert(args0Value.Length, "%")}";
+ return $"({left}) LIKE concat('%', {args0Value}, '%')";
+ case "ToLower": return $"lower({left})";
+ case "ToUpper": return $"upper({left})";
+ case "Substring":
+ var substrArgs1 = getExp(exp.Arguments[0]);
+ if (long.TryParse(substrArgs1, out var testtrylng1)) substrArgs1 = (testtrylng1 + 1).ToString();
+ else substrArgs1 += "+1";
+ if (exp.Arguments.Count == 1) return $"substr({left}, {substrArgs1})";
+ return $"substr({left}, {substrArgs1}, {getExp(exp.Arguments[1])})";
+ case "IndexOf":
+ var indexOfFindStr = getExp(exp.Arguments[0]);
+ if (exp.Arguments.Count > 1 && exp.Arguments[1].Type.FullName == "System.Int32")
+ {
+ var locateArgs1 = getExp(exp.Arguments[1]);
+ if (long.TryParse(locateArgs1, out var testtrylng2)) locateArgs1 = (testtrylng2 + 1).ToString();
+ else locateArgs1 += "+1";
+ return $"(locate({indexOfFindStr}, {left}, {locateArgs1})-1)";
+ }
+ return $"(locate({indexOfFindStr}, {left})-1)";
+ case "PadLeft":
+ if (exp.Arguments.Count == 1) return $"lpad({left}, {getExp(exp.Arguments[0])})";
+ return $"lpad({left}, {getExp(exp.Arguments[0])}, {getExp(exp.Arguments[1])})";
+ case "PadRight":
+ if (exp.Arguments.Count == 1) return $"rpad({left}, {getExp(exp.Arguments[0])})";
+ return $"rpad({left}, {getExp(exp.Arguments[0])}, {getExp(exp.Arguments[1])})";
+ case "Trim":
+ case "TrimStart":
+ case "TrimEnd":
+ if (exp.Arguments.Count == 0)
+ {
+ if (exp.Method.Name == "Trim") return $"trim({left})";
+ if (exp.Method.Name == "TrimStart") return $"ltrim({left})";
+ if (exp.Method.Name == "TrimEnd") return $"rtrim({left})";
+ }
+ foreach (var argsTrim02 in exp.Arguments)
+ {
+ var argsTrim01s = new[] { argsTrim02 };
+ if (argsTrim02.NodeType == ExpressionType.NewArrayInit)
+ {
+ var arritem = argsTrim02 as NewArrayExpression;
+ argsTrim01s = arritem.Expressions.ToArray();
+ }
+ foreach (var argsTrim01 in argsTrim01s)
+ {
+ if (exp.Method.Name == "Trim") left = $"trim({getExp(argsTrim01)} from {left})";
+ if (exp.Method.Name == "TrimStart") left = $"trim(leading {getExp(argsTrim01)} from {left})";
+ if (exp.Method.Name == "TrimEnd") left = $"trim(trailing {getExp(argsTrim01)} from {left})";
+ }
+ }
+ return left;
+ case "Replace": return $"replace({left}, {getExp(exp.Arguments[0])}, {getExp(exp.Arguments[1])})";
+ case "CompareTo": return $"strcmp({left}, {getExp(exp.Arguments[0])})";
+ case "Equals": return $"({left} = {getExp(exp.Arguments[0])})";
+ }
+ }
+ return null;
+ }
+ public override string ExpressionLambdaToSqlCallMath(MethodCallExpression exp, ExpTSC tsc)
+ {
+ Func getExp = exparg => ExpressionLambdaToSql(exparg, tsc);
+ switch (exp.Method.Name)
+ {
+ case "Abs": return $"abs({getExp(exp.Arguments[0])})";
+ case "Sign": return $"sign({getExp(exp.Arguments[0])})";
+ case "Floor": return $"floor({getExp(exp.Arguments[0])})";
+ case "Ceiling": return $"ceiling({getExp(exp.Arguments[0])})";
+ case "Round":
+ if (exp.Arguments.Count > 1 && exp.Arguments[1].Type.FullName == "System.Int32") return $"round({getExp(exp.Arguments[0])}, {getExp(exp.Arguments[1])})";
+ return $"round({getExp(exp.Arguments[0])})";
+ case "Exp": return $"exp({getExp(exp.Arguments[0])})";
+ case "Log": return $"log({getExp(exp.Arguments[0])})";
+ case "Log10": return $"log10({getExp(exp.Arguments[0])})";
+ case "Pow": return $"pow({getExp(exp.Arguments[0])}, {getExp(exp.Arguments[1])})";
+ case "Sqrt": return $"sqrt({getExp(exp.Arguments[0])})";
+ case "Cos": return $"cos({getExp(exp.Arguments[0])})";
+ case "Sin": return $"sin({getExp(exp.Arguments[0])})";
+ case "Tan": return $"tan({getExp(exp.Arguments[0])})";
+ case "Acos": return $"acos({getExp(exp.Arguments[0])})";
+ case "Asin": return $"asin({getExp(exp.Arguments[0])})";
+ case "Atan": return $"atan({getExp(exp.Arguments[0])})";
+ case "Atan2": return $"atan2({getExp(exp.Arguments[0])}, {getExp(exp.Arguments[1])})";
+ case "Truncate": return $"truncate({getExp(exp.Arguments[0])}, 0)";
+ }
+ return null;
+ }
+ public override string ExpressionLambdaToSqlCallDateTime(MethodCallExpression exp, ExpTSC tsc)
+ {
+ Func getExp = exparg => ExpressionLambdaToSql(exparg, tsc);
+ if (exp.Object == null)
+ {
+
+ switch (exp.Method.Name)
+ {
+ case "Compare": return $"({getExp(exp.Arguments[0])} - ({getExp(exp.Arguments[1])}))";
+ case "DaysInMonth": return $"toDayOfMonth(subtractDays(addMonths(toStartOfMonth(concat({getExp(exp.Arguments[0])}, '-', {getExp(exp.Arguments[1])}, '-01')), 1), 1))";
+ case "Equals": return $"({getExp(exp.Arguments[0])} = {getExp(exp.Arguments[1])})";
+
+ case "IsLeapYear":
+ var isLeapYearArgs1 = getExp(exp.Arguments[0]);
+ return $"(({isLeapYearArgs1})%4=0 AND ({isLeapYearArgs1})%100<>0 OR ({isLeapYearArgs1})%400=0)";
+
+ case "Parse": return $"cast({getExp(exp.Arguments[0])} as DateTime)";
+ case "ParseExact":
+ case "TryParse":
+ case "TryParseExact": return $"cast({getExp(exp.Arguments[0])} as DateTime)";
+ }
+ }
+ else
+ {
+ var left = getExp(exp.Object);
+ var args1 = exp.Arguments.Count == 0 ? null : getExp(exp.Arguments[0]);
+ switch (exp.Method.Name)
+ {
+ case "Add": return $"addSeconds(toDateTime({left}), {args1})";
+ case "AddDays": return $"addDays(toDateTime({left}), {args1})";
+ case "AddHours": return $"addHours(toDateTime({left}), {args1})";
+ case "AddMilliseconds": return $"addSeconds(toDateTime({left}), {args1}/1000)";
+ case "AddMinutes": return $"addMinutes(toDateTime({left}),{args1})";
+ case "AddMonths": return $"addMonths(toDateTime({left}),{args1})";
+ case "AddSeconds": return $"addSeconds(toDateTime({left}),{args1})";
+ case "AddTicks": return $"addSeconds(toDateTime({left}), {args1}/10000000)";
+ case "AddYears": return $"addYears(toDateTime({left}),{args1})";
+ case "Subtract":
+ switch ((exp.Arguments[0].Type.IsNullableType() ? exp.Arguments[0].Type.GetGenericArguments().FirstOrDefault() : exp.Arguments[0].Type).FullName)
+ {
+ case "System.DateTime": return $"dateDiff(second, {args1}, toDateTime({left}))";
+ case "System.TimeSpan": return $"addSeconds(toDateTime({left}),(({args1})*-1))";
+ }
+ break;
+ case "Equals": return $"({left} = {args1})";
+ case "CompareTo": return $"dateDiff(second,{args1},toDateTime({left}))";
+ case "ToString":
+ if (exp.Arguments.Count == 0) return $"date_format({left},'%Y-%m-%d %H:%M:%S.%f')";
+ switch (args1)
+ {
+ case "'yyyy-MM-dd HH:mm:ss'": return $"formatDateTime(toDateTime({left}),'%Y-%m-%d %H:%M:%S')";
+ case "'yyyy-MM-dd HH:mm'": return $"formatDateTime(toDateTime({left}),'%Y-%m-%d %H:%M')";
+ case "'yyyy-MM-dd HH'": return $"formatDateTime(toDateTime({left}),'%Y-%m-%d %H')";
+ case "'yyyy-MM-dd'": return $"formatDateTime(toDateTime({left}),'%Y-%m-%d')";
+ case "'yyyy-MM'": return $"formatDateTime(toDateTime({left}),'%Y-%m')";
+ case "'yyyyMMddHHmmss'": return $"formatDateTime(toDateTime({left}),'%Y%m%d%H%M%S')";
+ case "'yyyyMMddHHmm'": return $"formatDateTime(toDateTime({left}),'%Y%m%d%H%M')";
+ case "'yyyyMMddHH'": return $"formatDateTime(toDateTime({left}),'%Y%m%d%H')";
+ case "'yyyyMMdd'": return $"formatDateTime(toDateTime({left}),'%Y%m%d')";
+ case "'yyyyMM'": return $"formatDateTime(toDateTime({left}),'%Y%m')";
+ case "'yyyy'": return $"formatDateTime(toDateTime({left}),'%Y')";
+ case "'HH:mm:ss'": return $"formatDateTime(toDateTime({left}),'%H:%M:%S')";
+ }
+ args1 = Regex.Replace(args1, "(yyyy|yy|MM|M|dd|d|HH|H|hh|h|mm|ss|tt)", m =>
+ {
+ switch (m.Groups[1].Value)
+ {
+ case "yyyy": return $"%Y";
+ case "yy": return $"%y";
+ case "MM": return $"%_a1";
+ case "M": return $"%c";
+ case "dd": return $"%d";
+ case "d": return $"%e";
+ case "HH": return $"%H";
+ case "H": return $"%k";
+ case "hh": return $"%h";
+ case "h": return $"%l";
+ case "mm": return $"%i";
+ case "ss": return $"%_a2";
+ case "tt": return $"%p";
+ }
+ return m.Groups[0].Value;
+ });
+ var argsFinds = new[] { "%Y", "%y", "%_a1", "%c", "%d", "%e", "%H", "%k", "%h", "%l", "%M", "%_a2", "%p" };
+ var argsSpts = Regex.Split(args1, "(m|s|t)");
+ for (var a = 0; a < argsSpts.Length; a++)
+ {
+ switch (argsSpts[a])
+ {
+ case "m": argsSpts[a] = $"case when substr(formatDateTime(toDateTime({left}),'%M'),1,1) = '0' then substr(formatDateTime(toDateTime({left}),'%M'),2,1) else formatDateTime(toDateTime({left}),'%M') end"; break;
+ case "s": argsSpts[a] = $"case when substr(formatDateTime(toDateTime({left}),'%S'),1,1) = '0' then substr(formatDateTime(toDateTime({left}),'%S'),2,1) else formatDateTime(toDateTime({left}),'%S') end"; break;
+ case "t": argsSpts[a] = $"trim(trailing 'M' from formatDateTime(toDateTime({left}),'%p'))"; break;
+ default:
+ var argsSptsA = argsSpts[a];
+ if (argsSptsA.StartsWith("'")) argsSptsA = argsSptsA.Substring(1);
+ if (argsSptsA.EndsWith("'")) argsSptsA = argsSptsA.Remove(argsSptsA.Length - 1);
+ argsSpts[a] = argsFinds.Any(m => argsSptsA.Contains(m)) ? $"formatDateTime(toDateTime({left}),'{argsSptsA}')" : $"'{argsSptsA}'";
+ break;
+ }
+ }
+ if (argsSpts.Length > 0) args1 = $"concat({string.Join(", ", argsSpts.Where(a => a != "''"))})";
+ return args1.Replace("%_a1", "%m").Replace("%_a2", "%S");
+ }
+ }
+ return null;
+ }
+ public override string ExpressionLambdaToSqlCallTimeSpan(MethodCallExpression exp, ExpTSC tsc)
+ {
+ Func getExp = exparg => ExpressionLambdaToSql(exparg, tsc);
+ if (exp.Object == null)
+ {
+ switch (exp.Method.Name)
+ {
+ case "Compare": return $"({getExp(exp.Arguments[0])}-({getExp(exp.Arguments[1])}))";
+ case "Equals": return $"({getExp(exp.Arguments[0])} = {getExp(exp.Arguments[1])})";
+ case "FromDays": return $"(({getExp(exp.Arguments[0])})*{(long)1000000 * 60 * 60 * 24})";
+ case "FromHours": return $"(({getExp(exp.Arguments[0])})*{(long)1000000 * 60 * 60})";
+ case "FromMilliseconds": return $"(({getExp(exp.Arguments[0])})*1000)";
+ case "FromMinutes": return $"(({getExp(exp.Arguments[0])})*{(long)1000000 * 60})";
+ case "FromSeconds": return $"(({getExp(exp.Arguments[0])})*1000000)";
+ case "FromTicks": return $"(({getExp(exp.Arguments[0])})/10)";
+ case "Parse": return $"cast({getExp(exp.Arguments[0])} as Int64)";
+ case "ParseExact":
+ case "TryParse":
+ case "TryParseExact": return $"cast({getExp(exp.Arguments[0])} as Int64)";
+ }
+ }
+ else
+ {
+ var left = getExp(exp.Object);
+ var args1 = exp.Arguments.Count == 0 ? null : getExp(exp.Arguments[0]);
+ switch (exp.Method.Name)
+ {
+ case "Add": return $"({left}+{args1})";
+ case "Subtract": return $"({left}-({args1}))";
+ case "Equals": return $"({left} = {args1})";
+ case "CompareTo": return $"({left}-({args1}))";
+ case "ToString": return $"cast({left} as String)";
+ }
+ }
+ return null;
+ }
+ public override string ExpressionLambdaToSqlCallConvert(MethodCallExpression exp, ExpTSC tsc)
+ {
+ Func getExp = exparg => ExpressionLambdaToSql(exparg, tsc);
+ if (exp.Object == null)
+ {
+ switch (exp.Method.Name)
+ {
+ case "ToBoolean": return $"({getExp(exp.Arguments[0])} not in ('0','false'))";
+ case "ToByte": return $"cast({getExp(exp.Arguments[0])} as Int8)";
+ case "ToChar": return $"substr(cast({getExp(exp.Arguments[0])} as String), 1, 1)";
+ case "ToDateTime": return $"cast({getExp(exp.Arguments[0])} as DateTime)";
+ case "ToDecimal": return $"cast({getExp(exp.Arguments[0])} as Decimal128(19))";
+ case "ToDouble": return $"cast({getExp(exp.Arguments[0])} as Float64)";
+ case "ToInt16":
+ case "ToInt32":
+ case "ToInt64":
+ case "ToSByte": return $"cast({getExp(exp.Arguments[0])} as UInt8)";
+ case "ToSingle": return $"cast({getExp(exp.Arguments[0])} as Float32)";
+ case "ToString": return $"cast({getExp(exp.Arguments[0])} as String)";
+ case "ToUInt16":
+ case "ToUInt32":
+ case "ToUInt64": return $"cast({getExp(exp.Arguments[0])} as UInt64)";
+ }
+ }
+ return null;
+ }
+ }
+}
diff --git a/Providers/FreeSql.Provider.ClickHouse/ClickHouseExtensions.cs b/Providers/FreeSql.Provider.ClickHouse/ClickHouseExtensions.cs
new file mode 100644
index 00000000..d0f362a5
--- /dev/null
+++ b/Providers/FreeSql.Provider.ClickHouse/ClickHouseExtensions.cs
@@ -0,0 +1,18 @@
+using FreeSql;
+using FreeSql.ClickHouse.Curd;
+using System;
+
+public static partial class FreeSqlClickHouseGlobalExtensions
+{
+
+ ///
+ /// 特殊处理类似 string.Format 的使用方法,防止注入,以及 IS NULL 转换
+ ///
+ ///
+ ///
+ ///
+ public static string FormatClickHouse(this string that, params object[] args) => _clickHouseAdo.Addslashes(that, args);
+ static FreeSql.ClickHouse.ClickHouseAdo _clickHouseAdo = new FreeSql.ClickHouse.ClickHouseAdo();
+
+
+}
diff --git a/Providers/FreeSql.Provider.ClickHouse/ClickHouseProvider.cs b/Providers/FreeSql.Provider.ClickHouse/ClickHouseProvider.cs
new file mode 100644
index 00000000..41ff2dba
--- /dev/null
+++ b/Providers/FreeSql.Provider.ClickHouse/ClickHouseProvider.cs
@@ -0,0 +1,42 @@
+using FreeSql.Internal;
+using FreeSql.Internal.CommonProvider;
+using FreeSql.ClickHouse.Curd;
+using System;
+using System.Data.Common;
+using System.Linq.Expressions;
+using System.Threading;
+
+namespace FreeSql.ClickHouse
+{
+
+ public class ClickHouseProvider : BaseDbProvider, IFreeSql
+ {
+ public override ISelect CreateSelectProvider(object dywhere) => new ClickHouseSelect(this, this.InternalCommonUtils, this.InternalCommonExpression, dywhere);
+ public override IInsert CreateInsertProvider() => new ClickHouseInsert(this, this.InternalCommonUtils, this.InternalCommonExpression);
+ public override IUpdate CreateUpdateProvider(object dywhere) => new ClickHouseUpdate(this, this.InternalCommonUtils, this.InternalCommonExpression, dywhere);
+ public override IDelete CreateDeleteProvider(object dywhere) => new ClickHouseDelete(this, this.InternalCommonUtils, this.InternalCommonExpression, dywhere);
+ public override IInsertOrUpdate CreateInsertOrUpdateProvider()
+ {
+ throw new NotImplementedException();
+ }
+ public ClickHouseProvider(string masterConnectionString, string[] slaveConnectionString, Func connectionFactory = null)
+ {
+ this.InternalCommonUtils = new ClickHouseUtils(this);
+ this.InternalCommonExpression = new ClickHouseExpression(this.InternalCommonUtils);
+
+ this.Ado = new ClickHouseAdo(this.InternalCommonUtils, masterConnectionString, slaveConnectionString, connectionFactory);
+ this.Aop = new AopProvider();
+
+ this.DbFirst = new ClickHouseDbFirst(this, this.InternalCommonUtils, this.InternalCommonExpression);
+ this.CodeFirst = new ClickHouseCodeFirst(this, this.InternalCommonUtils, this.InternalCommonExpression);
+ }
+
+ ~ClickHouseProvider() => this.Dispose();
+ int _disposeCounter;
+ public override void Dispose()
+ {
+ if (Interlocked.Increment(ref _disposeCounter) != 1) return;
+ (this.Ado as AdoProvider)?.Dispose();
+ }
+ }
+}
diff --git a/Providers/FreeSql.Provider.ClickHouse/ClickHouseUtils.cs b/Providers/FreeSql.Provider.ClickHouse/ClickHouseUtils.cs
new file mode 100644
index 00000000..9bb6d484
--- /dev/null
+++ b/Providers/FreeSql.Provider.ClickHouse/ClickHouseUtils.cs
@@ -0,0 +1,138 @@
+using FreeSql.Internal;
+using FreeSql.Internal.Model;
+using ClickHouse.Client.ADO;
+using System;
+using System.Collections.Generic;
+using System.Data.Common;
+using System.Globalization;
+using System.Data;
+using ClickHouse.Client.ADO.Parameters;
+
+namespace FreeSql.ClickHouse
+{
+
+ class ClickHouseUtils : CommonUtils
+ {
+ public ClickHouseUtils(IFreeSql orm) : base(orm)
+ {
+ }
+
+ public override DbParameter AppendParamter(List _params, string parameterName, ColumnInfo col, Type type, object value)
+ {
+ if (string.IsNullOrEmpty(parameterName)) parameterName = $"p_{_params?.Count}";
+ var dbtype = (DbType)_orm.CodeFirst.GetDbInfo(type)?.type;
+ DbParameter ret = new ClickHouseDbParameter { ParameterName = parameterName, DbType= dbtype, Value = value };//QuoteParamterName(parameterName)
+ if (col != null)
+ {
+ var dbtype2 = (DbType)_orm.DbFirst.GetDbType(new DatabaseModel.DbColumnInfo { DbTypeText = col.DbTypeText, DbTypeTextFull = col.Attribute.DbType, MaxLength = col.DbSize });
+ switch (dbtype2)
+ {
+ case DbType.Binary:
+ default:
+ dbtype = dbtype2;
+ //if (col.DbSize != 0) ret.Size = col.DbSize;
+ if (col.DbPrecision != 0) ret.Precision = col.DbPrecision;
+ if (col.DbScale != 0) ret.Scale = col.DbScale;
+ break;
+ }
+ if (value is bool)
+ {
+ ret.Value = (bool)value ? 1 : 0;
+ }
+ }
+
+ ret.DbType = dbtype;
+ _params?.Add(ret);
+ return ret;
+ }
+
+ public override DbParameter[] GetDbParamtersByObject(string sql, object obj) =>
+ Utils.GetDbParamtersByObject(sql, obj, "?", (name, type, value) =>
+ {
+ DbParameter ret = new ClickHouseDbParameter { ParameterName = $"?{name}", Value = value };
+ var tp = _orm.CodeFirst.GetDbInfo(type)?.type;
+ if (tp != null)
+ {
+
+ ret.DbType = (DbType)tp.Value;
+ }
+ return ret;
+ });
+ public override string RewriteColumn(ColumnInfo col, string sql)
+ {
+ col.Attribute.DbType = col.Attribute.DbType.Replace(" NOT NULL", "");
+ if (string.IsNullOrWhiteSpace(col?.Attribute.RewriteSql) == false)
+ return string.Format(col.Attribute.RewriteSql, sql);
+ return string.Format(sql, col.Attribute.DbType);
+ }
+ public override string FormatSql(string sql, params object[] args) => sql?.FormatClickHouse(args);
+ public override string QuoteSqlName(params string[] name)
+ {
+ if (name.Length == 1)
+ {
+ var nametrim = name[0].Trim();
+ if (nametrim.StartsWith("(") && nametrim.EndsWith(")"))
+ return nametrim; //原生SQL
+ if (nametrim.StartsWith("`") && nametrim.EndsWith("`"))
+ return nametrim;
+ return $"`{nametrim.Replace(".", "`.`")}`";
+ }
+ return $"`{string.Join("`.`", name)}`";
+ }
+ public override string TrimQuoteSqlName(string name)
+ {
+ var nametrim = name.Trim();
+ if (nametrim.StartsWith("(") && nametrim.EndsWith(")"))
+ return nametrim; //原生SQL
+ return $"{nametrim.Trim('`').Replace("`.`", ".").Replace(".`", ".")}";
+ }
+ public override string[] SplitTableName(string name) => GetSplitTableNames(name, '`', '`', 2);
+ public override string QuoteParamterName(string name) => $"{{{{{name}:{{0}}}}}}";
+ public override string IsNull(string sql, object value) => $"ifnull({sql}, {value})";
+ public override string StringConcat(string[] objs, Type[] types) => $"concat({string.Join(", ", objs)})";
+ public override string Mod(string left, string right, Type leftType, Type rightType) => $"{left} % {right}";
+ public override string Div(string left, string right, Type leftType, Type rightType) => $"{left} div {right}";
+ public override string Now => "now()";
+ public override string NowUtc => "now('UTC')";
+
+ public override string QuoteWriteParamterAdapter(Type type, string paramterName)
+ {
+ switch (type.FullName)
+ {
+ case "MygisPoint":
+ case "MygisLineString":
+ case "MygisPolygon":
+ case "MygisMultiPoint":
+ case "MygisMultiLineString":
+ case "MygisMultiPolygon": return $"ST_GeomFromText({paramterName})";
+ }
+ return paramterName;
+ }
+ protected override string QuoteReadColumnAdapter(Type type, Type mapType, string columnName)
+ {
+ switch (mapType.FullName)
+ {
+ case "MygisPoint":
+ case "MygisLineString":
+ case "MygisPolygon":
+ case "MygisMultiPoint":
+ case "MygisMultiLineString":
+ case "MygisMultiPolygon": return $"ST_AsText({columnName})";
+ }
+ return columnName;
+ }
+
+ public override string GetNoneParamaterSqlValue(List specialParams, string specialParamFlag, ColumnInfo col, Type type, object value)
+ {
+ if (value == null) return "NULL";
+ if (type.IsNumberType()) return string.Format(CultureInfo.InvariantCulture, "{0}", value);
+ if (type == typeof(byte[])) return $"0x{CommonUtils.BytesSqlRaw(value as byte[])}";
+ if (type == typeof(TimeSpan) || type == typeof(TimeSpan?))
+ {
+ var ts = (TimeSpan)value;
+ value = $"{Math.Floor(ts.TotalHours)}:{ts.Minutes}:{ts.Seconds}";
+ }
+ return FormatSql("{0}", value, 1);
+ }
+ }
+}
diff --git a/Providers/FreeSql.Provider.ClickHouse/Curd/ClickHouseDelete.cs b/Providers/FreeSql.Provider.ClickHouse/Curd/ClickHouseDelete.cs
new file mode 100644
index 00000000..c950f12b
--- /dev/null
+++ b/Providers/FreeSql.Provider.ClickHouse/Curd/ClickHouseDelete.cs
@@ -0,0 +1,35 @@
+using FreeSql.Internal;
+using System;
+using System.Collections.Generic;
+using System.Data;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace FreeSql.ClickHouse.Curd
+{
+
+ class ClickHouseDelete : Internal.CommonProvider.DeleteProvider
+ {
+ public ClickHouseDelete(IFreeSql orm, CommonUtils commonUtils, CommonExpression commonExpression, object dywhere)
+ : base(orm, commonUtils, commonExpression, dywhere)
+ {
+ }
+
+ public override List ExecuteDeleted()
+ {
+ throw new NotImplementedException("FreeSql.ClickHouse.Curd 暂未实现");
+ }
+ public override string ToSql()
+ {
+ return base.ToSql().Replace("DELETE FROM", "ALTER TABLE").Replace("WHERE", "DELETE WHERE");
+ }
+#if net40
+#else
+ public override Task> ExecuteDeletedAsync(CancellationToken cancellationToken = default)
+ {
+ throw new NotImplementedException("FreeSql.ClickHouse.Curd 暂未实现");
+ }
+#endif
+ }
+}
diff --git a/Providers/FreeSql.Provider.ClickHouse/Curd/ClickHouseInsert.cs b/Providers/FreeSql.Provider.ClickHouse/Curd/ClickHouseInsert.cs
new file mode 100644
index 00000000..ce6b2f9c
--- /dev/null
+++ b/Providers/FreeSql.Provider.ClickHouse/Curd/ClickHouseInsert.cs
@@ -0,0 +1,415 @@
+using ClickHouse.Client.ADO;
+using ClickHouse.Client.Copy;
+using FreeSql.Internal;
+using FreeSql.Internal.Model;
+using System;
+using System.Collections.Generic;
+using System.Data;
+using System.Data.Common;
+using System.Diagnostics;
+using System.Linq;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace FreeSql.ClickHouse.Curd
+{
+
+ class ClickHouseInsert : Internal.CommonProvider.InsertProvider where T1 : class
+ {
+ public ClickHouseInsert(IFreeSql orm, CommonUtils commonUtils, CommonExpression commonExpression)
+ : base(orm, commonUtils, commonExpression)
+ {
+ }
+
+ internal bool InternalIsIgnoreInto = false;
+ internal IFreeSql InternalOrm => _orm;
+ internal TableInfo InternalTable => _table;
+ internal DbParameter[] InternalParams => _params;
+ internal DbConnection InternalConnection => _connection;
+ internal DbTransaction InternalTransaction => _transaction;
+ internal CommonUtils InternalCommonUtils => _commonUtils;
+ internal CommonExpression InternalCommonExpression => _commonExpression;
+ internal List InternalSource => _source;
+ internal Dictionary InternalIgnore => _ignore;
+ internal void InternalClearData() => ClearData();
+
+ public override int ExecuteAffrows() => SplitExecuteAffrows(_batchValuesLimit > 0 ? _batchValuesLimit : int.MaxValue, _batchParameterLimit > 0 ? _batchParameterLimit : int.MaxValue);
+ public override long ExecuteIdentity() => SplitExecuteIdentity(_batchValuesLimit > 0 ? _batchValuesLimit : int.MaxValue, _batchParameterLimit > 0 ? _batchParameterLimit : int.MaxValue);
+ public override List ExecuteInserted() => SplitExecuteInserted(_batchValuesLimit > 0 ? _batchValuesLimit : int.MaxValue, _batchParameterLimit > 0 ? _batchParameterLimit : int.MaxValue);
+
+
+ public override string ToSql()
+ {
+ if (InternalIsIgnoreInto == false) return base.ToSqlValuesOrSelectUnionAll();
+ var sql = base.ToSqlValuesOrSelectUnionAll();
+ return $"INSERT IGNORE INTO {sql.Substring(12)}";
+ }
+
+ protected override int RawExecuteAffrows()
+ {
+ var affrows = 0;
+ Exception exception = null;
+ Aop.CurdBeforeEventArgs before=null;
+ if (_source.Count>1)
+ {
+ try
+ {
+ before = new Aop.CurdBeforeEventArgs(_table.Type, _table, Aop.CurdType.Insert, null, _params);
+ _orm.Aop.CurdBeforeHandler?.Invoke(this, before);
+ using var bulkCopyInterface = new ClickHouseBulkCopy(_orm.Ado.MasterPool.Get().Value as ClickHouseConnection)
+ {
+ DestinationTableName = _table.DbName,
+ BatchSize = _source.Count
+ };
+ var data=ToDataTable();
+ bulkCopyInterface.WriteToServerAsync(data, default).Wait();
+ return affrows;
+ }
+ catch (Exception ex)
+ {
+ exception = ex;
+ throw ex;
+ }
+ finally
+ {
+ var after = new Aop.CurdAfterEventArgs(before, exception, affrows);
+ _orm.Aop.CurdAfterHandler?.Invoke(this, after);
+ }
+ }
+ else
+ {
+ var sql = this.ToSql();
+ before = new Aop.CurdBeforeEventArgs(_table.Type, _table, Aop.CurdType.Insert, sql, _params);
+ _orm.Aop.CurdBeforeHandler?.Invoke(this, before);
+ try
+ {
+ affrows = _orm.Ado.ExecuteNonQuery(_connection, null, CommandType.Text, sql, _commandTimeout, _params);
+ }
+ catch (Exception ex)
+ {
+ exception = ex;
+ throw ex;
+ }
+ finally
+ {
+ var after = new Aop.CurdAfterEventArgs(before, exception, affrows);
+ _orm.Aop.CurdAfterHandler?.Invoke(this, after);
+ }
+ return affrows;
+ }
+ }
+
+ private IDictionary GetValue(T u, System.Reflection.PropertyInfo[] columns)
+ {
+ try
+ {
+ Dictionary dic = new Dictionary();
+ foreach (var item in columns)
+ {
+ object v = null;
+ if (u != null)
+ {
+ v = item.GetValue(u);
+ }
+ dic.TryAdd(item.Name, v);
+ }
+ return dic;
+ }
+ catch (Exception e)
+ {
+ throw;
+ }
+ }
+
+ protected override long RawExecuteIdentity()
+ {
+ long ret = 0;
+ var identCols = _table.Columns.Where(a => a.Value.Attribute.IsIdentity == true);
+ if (identCols.Any()&&_source.Count==1)
+ ret = (long)identCols.First().Value.GetValue(_source.First());
+ return ret;
+ }
+ protected override List RawExecuteInserted()
+ {
+ var sql = this.ToSql();
+ if (string.IsNullOrEmpty(sql)) return new List();
+
+ var sb = new StringBuilder();
+ sb.Append(sql).Append(" RETURNING ");
+
+ var colidx = 0;
+ foreach (var col in _table.Columns.Values)
+ {
+ if (colidx > 0) sb.Append(", ");
+ sb.Append(_commonUtils.RereadColumn(col, _commonUtils.QuoteSqlName(col.Attribute.Name))).Append(" as ").Append(_commonUtils.QuoteSqlName(col.CsName));
+ ++colidx;
+ }
+ sql = sb.ToString();
+ var before = new Aop.CurdBeforeEventArgs(_table.Type, _table, Aop.CurdType.Insert, sql, _params);
+ _orm.Aop.CurdBeforeHandler?.Invoke(this, before);
+ var ret = new List();
+ Exception exception = null;
+ try
+ {
+ ret = _orm.Ado.Query(_table.TypeLazy ?? _table.Type, _connection, null, CommandType.Text, sql, _commandTimeout, _params);
+ }
+ catch (Exception ex)
+ {
+ exception = ex;
+ throw;
+ }
+ finally
+ {
+ var after = new Aop.CurdAfterEventArgs(before, exception, ret);
+ _orm.Aop.CurdAfterHandler?.Invoke(this, after);
+ }
+ return ret;
+ }
+
+#if net40
+#else
+ public override Task ExecuteAffrowsAsync(CancellationToken cancellationToken = default) => SplitExecuteAffrowsAsync(_batchValuesLimit > 0 ? _batchValuesLimit : int.MaxValue, _batchValuesLimit > 0 ? _batchValuesLimit : int.MaxValue, cancellationToken);
+ public override Task ExecuteIdentityAsync(CancellationToken cancellationToken = default) => SplitExecuteIdentityAsync(_batchValuesLimit > 0 ? _batchValuesLimit : int.MaxValue, _batchValuesLimit > 0 ? _batchValuesLimit : int.MaxValue, cancellationToken);
+ public override Task> ExecuteInsertedAsync(CancellationToken cancellationToken = default) => SplitExecuteInsertedAsync(_batchValuesLimit > 0 ? _batchValuesLimit : int.MaxValue, _batchValuesLimit > 0 ? _batchValuesLimit : int.MaxValue, cancellationToken);
+
+ async protected override Task RawExecuteIdentityAsync(CancellationToken cancellationToken = default)
+ {
+ //var sql = this.ToSql();
+ //if (string.IsNullOrEmpty(sql)) return 0;
+
+ //sql = string.Concat(sql, "; SELECT LAST_INSERT_ID();");
+ //var before = new Aop.CurdBeforeEventArgs(_table.Type, _table, Aop.CurdType.Insert, sql, _params);
+ //_orm.Aop.CurdBeforeHandler?.Invoke(this, before);
+ long ret = 0;
+ //Exception exception = null;
+ //try
+ //{
+ // ret = long.TryParse(string.Concat(await _orm.Ado.ExecuteScalarAsync(_connection, null, CommandType.Text, sql, _commandTimeout, _params, cancellationToken)), out var trylng) ? trylng : 0;
+ //}
+ //catch (Exception ex)
+ //{
+ // exception = ex;
+ // throw;
+ //}
+ //finally
+ //{
+ // var after = new Aop.CurdAfterEventArgs(before, exception, ret);
+ // _orm.Aop.CurdAfterHandler?.Invoke(this, after);
+ //}
+ return await Task.FromResult(ret);
+ }
+
+
+ protected override int SplitExecuteAffrows(int valuesLimit, int parameterLimit)
+ {
+ var ss = SplitSource(valuesLimit, parameterLimit);
+ var ret = 0;
+ if (ss.Any() == false)
+ {
+ ClearData();
+ return ret;
+ }
+ if (ss.Length == 1)
+ {
+ _batchProgress?.Invoke(new BatchProgressStatus(_source, 1, 1));
+ ret = this.RawExecuteAffrows();
+ ClearData();
+ return ret;
+ }
+ var before = new Aop.TraceBeforeEventArgs("SplitExecuteAffrows", null);
+ _orm.Aop.TraceBeforeHandler?.Invoke(this, before);
+ Exception exception = null;
+ try
+ {
+ for (var a = 0; a < ss.Length; a++)
+ {
+ _source = ss[a];
+ _batchProgress?.Invoke(new BatchProgressStatus(_source, a + 1, ss.Length));
+ ret += this.RawExecuteAffrows();
+ }
+ }
+ catch (Exception ex)
+ {
+ exception = ex;
+ throw;
+ }
+ finally
+ {
+ var after = new Aop.TraceAfterEventArgs(before, null, exception);
+ _orm.Aop.TraceAfterHandler?.Invoke(this, after);
+ }
+ ClearData();
+ return ret;
+ }
+ async protected override Task SplitExecuteAffrowsAsync(int valuesLimit, int parameterLimit, CancellationToken cancellationToken = default)
+ {
+ var ss = SplitSource(valuesLimit, parameterLimit);
+ var ret = 0;
+ if (ss.Any() == false)
+ {
+ ClearData();
+ return ret;
+ }
+ if (ss.Length == 1)
+ {
+ _batchProgress?.Invoke(new BatchProgressStatus(_source, 1, 1));
+ await this.RawExecuteAffrowsAsync(cancellationToken);
+ ret = _source.Count;
+ ClearData();
+ return ret;
+ }
+
+ var before = new Aop.TraceBeforeEventArgs("SplitExecuteAffrowsAsync", null);
+ _orm.Aop.TraceBeforeHandler?.Invoke(this, before);
+ Exception exception = null;
+ try
+ {
+ for (var a = 0; a < ss.Length; a++)
+ {
+ _source = ss[a];
+ _batchProgress?.Invoke(new BatchProgressStatus(_source, a + 1, ss.Length));
+ await this.RawExecuteAffrowsAsync(cancellationToken);
+ ret += _source.Count;
+ }
+ }
+ catch (Exception ex)
+ {
+ exception = ex;
+ throw;
+ }
+ finally
+ {
+ var after = new Aop.TraceAfterEventArgs(before, null, exception);
+ _orm.Aop.TraceAfterHandler?.Invoke(this, after);
+ }
+ ClearData();
+ return ret;
+ }
+
+ async protected override Task SplitExecuteIdentityAsync(int valuesLimit, int parameterLimit, CancellationToken cancellationToken = default)
+ {
+ var ss = SplitSource(valuesLimit, parameterLimit);
+ long ret = 0;
+ if (ss.Any() == false)
+ {
+ ClearData();
+ return ret;
+ }
+ if (ss.Length == 1)
+ {
+ _batchProgress?.Invoke(new BatchProgressStatus(_source, 1, 1));
+ ret = await this.RawExecuteIdentityAsync(cancellationToken);
+ ClearData();
+ return ret;
+ }
+
+ var before = new Aop.TraceBeforeEventArgs("SplitExecuteIdentityAsync", null);
+ _orm.Aop.TraceBeforeHandler?.Invoke(this, before);
+ Exception exception = null;
+ try
+ {
+ for (var a = 0; a < ss.Length; a++)
+ {
+ _source = ss[a];
+ _batchProgress?.Invoke(new BatchProgressStatus(_source, a + 1, ss.Length));
+ if (a < ss.Length - 1) await this.RawExecuteAffrowsAsync(cancellationToken);
+ else ret = await this.RawExecuteIdentityAsync(cancellationToken);
+ }
+ }
+ catch (Exception ex)
+ {
+ exception = ex;
+ throw;
+ }
+ finally
+ {
+ var after = new Aop.TraceAfterEventArgs(before, null, exception);
+ _orm.Aop.TraceAfterHandler?.Invoke(this, after);
+ }
+ ClearData();
+ return ret;
+ }
+
+ async protected override Task> SplitExecuteInsertedAsync(int valuesLimit, int parameterLimit, CancellationToken cancellationToken = default)
+ {
+ var ss = SplitSource(valuesLimit, parameterLimit);
+ var ret = new List();
+ if (ss.Any() == false)
+ {
+ ClearData();
+ return ret;
+ }
+ if (ss.Length == 1)
+ {
+ _batchProgress?.Invoke(new BatchProgressStatus(_source, 1, 1));
+ ret = await this.RawExecuteInsertedAsync(cancellationToken);
+ ClearData();
+ return ret;
+ }
+
+ var before = new Aop.TraceBeforeEventArgs("SplitExecuteInsertedAsync", null);
+ _orm.Aop.TraceBeforeHandler?.Invoke(this, before);
+ Exception exception = null;
+ try
+ {
+ for (var a = 0; a < ss.Length; a++)
+ {
+ _source = ss[a];
+ _batchProgress?.Invoke(new BatchProgressStatus(_source, a + 1, ss.Length));
+ ret.AddRange(await this.RawExecuteInsertedAsync(cancellationToken));
+ }
+ }
+ catch (Exception ex)
+ {
+ exception = ex;
+ throw;
+ }
+ finally
+ {
+ var after = new Aop.TraceAfterEventArgs(before, null, exception);
+ _orm.Aop.TraceAfterHandler?.Invoke(this, after);
+ }
+ ClearData();
+ return ret;
+ }
+
+ async protected override Task> RawExecuteInsertedAsync(CancellationToken cancellationToken = default)
+ {
+ var sql = this.ToSql();
+ if (string.IsNullOrEmpty(sql)) return new List();
+
+ var sb = new StringBuilder();
+ sb.Append(sql).Append(" RETURNING ");
+
+ var colidx = 0;
+ foreach (var col in _table.Columns.Values)
+ {
+ if (colidx > 0) sb.Append(", ");
+ sb.Append(_commonUtils.RereadColumn(col, _commonUtils.QuoteSqlName(col.Attribute.Name))).Append(" as ").Append(_commonUtils.QuoteSqlName(col.CsName));
+ ++colidx;
+ }
+ sql = sb.ToString();
+ var before = new Aop.CurdBeforeEventArgs(_table.Type, _table, Aop.CurdType.Insert, sql, _params);
+ _orm.Aop.CurdBeforeHandler?.Invoke(this, before);
+ var ret = new List();
+ Exception exception = null;
+ try
+ {
+ ret = await _orm.Ado.QueryAsync(_table.TypeLazy ?? _table.Type, _connection, null, CommandType.Text, sql, _commandTimeout, _params, cancellationToken);
+ }
+ catch (Exception ex)
+ {
+ exception = ex;
+ throw;
+ }
+ finally
+ {
+ var after = new Aop.CurdAfterEventArgs(before, exception, ret);
+ _orm.Aop.CurdAfterHandler?.Invoke(this, after);
+ }
+ return ret;
+ }
+#endif
+ }
+}
diff --git a/Providers/FreeSql.Provider.ClickHouse/Curd/ClickHouseSelect.cs b/Providers/FreeSql.Provider.ClickHouse/Curd/ClickHouseSelect.cs
new file mode 100644
index 00000000..0f018e6a
--- /dev/null
+++ b/Providers/FreeSql.Provider.ClickHouse/Curd/ClickHouseSelect.cs
@@ -0,0 +1,210 @@
+using FreeSql.Internal;
+using FreeSql.Internal.Model;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Linq.Expressions;
+using System.Text;
+
+namespace FreeSql.ClickHouse.Curd
+{
+
+ class ClickHouseSelect : FreeSql.Internal.CommonProvider.Select1Provider
+ {
+
+ internal static string ToSqlStatic(CommonUtils _commonUtils, CommonExpression _commonExpression, string _select, bool _distinct, string field, StringBuilder _join, StringBuilder _where, string _groupby, string _having, string _orderby, int _skip, int _limit, List _tables, List> tbUnions, Func _aliasRule, string _tosqlAppendContent, List _whereGlobalFilter, IFreeSql _orm)
+ {
+ if (_orm.CodeFirst.IsAutoSyncStructure)
+ _orm.CodeFirst.SyncStructure(_tables.Select(a => a.Table.Type).ToArray());
+
+ if (_whereGlobalFilter.Any())
+ foreach (var tb in _tables.Where(a => a.Type != SelectTableInfoType.Parent))
+ tb.Cascade = _commonExpression.GetWhereCascadeSql(tb, _whereGlobalFilter, true);
+
+ var sb = new StringBuilder();
+ var tbUnionsGt0 = tbUnions.Count > 1;
+ for (var tbUnionsIdx = 0; tbUnionsIdx < tbUnions.Count; tbUnionsIdx++)
+ {
+ if (tbUnionsIdx > 0) sb.Append("\r\n \r\nUNION ALL\r\n \r\n");
+ if (tbUnionsGt0) sb.Append(_select).Append(" * from (");
+ var tbUnion = tbUnions[tbUnionsIdx];
+
+ var sbnav = new StringBuilder();
+ sb.Append(_select);
+ if (_distinct) sb.Append("DISTINCT ");
+ sb.Append(field).Append(" \r\nFROM ");
+ var tbsjoin = _tables.Where(a => a.Type != SelectTableInfoType.From).ToArray();
+ var tbsfrom = _tables.Where(a => a.Type == SelectTableInfoType.From).ToArray();
+ for (var a = 0; a < tbsfrom.Length; a++)
+ {
+ sb.Append(_commonUtils.QuoteSqlName(tbUnion[tbsfrom[a].Table.Type])).Append(" ").Append(_aliasRule?.Invoke(tbsfrom[a].Table.Type, tbsfrom[a].Alias) ?? tbsfrom[a].Alias);
+ if (tbsjoin.Length > 0)
+ {
+ //如果存在 join 查询,则处理 from t1, t2 改为 from t1 inner join t2 on 1 = 1
+ for (var b = 1; b < tbsfrom.Length; b++)
+ {
+ sb.Append(" \r\nLEFT JOIN ").Append(_commonUtils.QuoteSqlName(tbUnion[tbsfrom[b].Table.Type])).Append(" ").Append(_aliasRule?.Invoke(tbsfrom[b].Table.Type, tbsfrom[b].Alias) ?? tbsfrom[b].Alias);
+
+ if (string.IsNullOrEmpty(tbsfrom[b].NavigateCondition) && string.IsNullOrEmpty(tbsfrom[b].On) && string.IsNullOrEmpty(tbsfrom[b].Cascade)) sb.Append(" ON 1 = 1");
+ else
+ {
+ var onSql = tbsfrom[b].NavigateCondition ?? tbsfrom[b].On;
+ sb.Append(" ON ").Append(onSql);
+ if (string.IsNullOrEmpty(tbsfrom[b].Cascade) == false)
+ {
+ if (string.IsNullOrEmpty(onSql)) sb.Append(tbsfrom[b].Cascade);
+ else sb.Append(" AND ").Append(tbsfrom[b].Cascade);
+ }
+ }
+ }
+ break;
+ }
+ else
+ {
+ if (!string.IsNullOrEmpty(tbsfrom[a].NavigateCondition)) sbnav.Append(" AND (").Append(tbsfrom[a].NavigateCondition).Append(")");
+ if (!string.IsNullOrEmpty(tbsfrom[a].On)) sbnav.Append(" AND (").Append(tbsfrom[a].On).Append(")");
+ if (a > 0 && !string.IsNullOrEmpty(tbsfrom[a].Cascade)) sbnav.Append(" AND ").Append(tbsfrom[a].Cascade);
+ }
+ if (a < tbsfrom.Length - 1) sb.Append(", ");
+ }
+ foreach (var tb in tbsjoin)
+ {
+ if (tb.Type == SelectTableInfoType.Parent) continue;
+ switch (tb.Type)
+ {
+ case SelectTableInfoType.LeftJoin:
+ sb.Append(" \r\nLEFT JOIN ");
+ break;
+ case SelectTableInfoType.InnerJoin:
+ sb.Append(" \r\nINNER JOIN ");
+ break;
+ case SelectTableInfoType.RightJoin:
+ sb.Append(" \r\nRIGHT JOIN ");
+ break;
+ }
+ sb.Append(_commonUtils.QuoteSqlName(tbUnion[tb.Table.Type])).Append(" ").Append(_aliasRule?.Invoke(tb.Table.Type, tb.Alias) ?? tb.Alias).Append(" ON ").Append(tb.On ?? tb.NavigateCondition);
+ if (!string.IsNullOrEmpty(tb.Cascade)) sb.Append(" AND ").Append(tb.Cascade);
+ if (!string.IsNullOrEmpty(tb.On) && !string.IsNullOrEmpty(tb.NavigateCondition)) sbnav.Append(" AND (").Append(tb.NavigateCondition).Append(")");
+ }
+ if (_join.Length > 0) sb.Append(_join);
+
+ sbnav.Append(_where);
+ if (!string.IsNullOrEmpty(_tables[0].Cascade))
+ sbnav.Append(" AND ").Append(_tables[0].Cascade);
+
+ if (sbnav.Length > 0)
+ {
+ sb.Append(" \r\nWHERE ").Append(sbnav.Remove(0, 5));
+ }
+ if (string.IsNullOrEmpty(_groupby) == false)
+ {
+ sb.Append(_groupby);
+ if (string.IsNullOrEmpty(_having) == false)
+ sb.Append(" \r\nHAVING ").Append(_having.Substring(5));
+ }
+ sb.Append(_orderby);
+ if (_skip > 0 || _limit > 0)
+ sb.Append(" \r\nlimit ").Append(Math.Max(0, _skip)).Append(",").Append(_limit > 0 ? _limit : -1);
+
+ sbnav.Clear();
+ if (tbUnionsGt0) sb.Append(") ftb");
+ }
+ return sb.Append(_tosqlAppendContent).ToString();
+ }
+
+ public ClickHouseSelect(IFreeSql orm, CommonUtils commonUtils, CommonExpression commonExpression, object dywhere) : base(orm, commonUtils, commonExpression, dywhere) { }
+ public override ISelect From(Expression, T2, ISelectFromExpression>> exp) { this.InternalFrom(exp); var ret = new ClickHouseSelect(_orm, _commonUtils, _commonExpression, null); ClickHouseSelect.CopyData(this, ret, exp?.Parameters); return ret; }
+ public override ISelect From(Expression, T2, T3, ISelectFromExpression>> exp) { this.InternalFrom(exp); var ret = new ClickHouseSelect(_orm, _commonUtils, _commonExpression, null); ClickHouseSelect.CopyData(this, ret, exp?.Parameters); return ret; }
+ public override ISelect From(Expression, T2, T3, T4, ISelectFromExpression>> exp) { this.InternalFrom(exp); var ret = new ClickHouseSelect(_orm, _commonUtils, _commonExpression, null); ClickHouseSelect.CopyData(this, ret, exp?.Parameters); return ret; }
+ public override ISelect From(Expression, T2, T3, T4, T5, ISelectFromExpression>> exp) { this.InternalFrom(exp); var ret = new ClickHouseSelect(_orm, _commonUtils, _commonExpression, null); ClickHouseSelect.CopyData(this, ret, exp?.Parameters); return ret; }
+ public override ISelect From(Expression, T2, T3, T4, T5, T6, ISelectFromExpression>> exp) { this.InternalFrom(exp); var ret = new ClickHouseSelect(_orm, _commonUtils, _commonExpression, null); ClickHouseSelect.CopyData(this, ret, exp?.Parameters); return ret; }
+ public override ISelect From(Expression, T2, T3, T4, T5, T6, T7, ISelectFromExpression>> exp) { this.InternalFrom(exp); var ret = new ClickHouseSelect(_orm, _commonUtils, _commonExpression, null); ClickHouseSelect.CopyData(this, ret, exp?.Parameters); return ret; }
+ public override ISelect From(Expression, T2, T3, T4, T5, T6, T7, T8, ISelectFromExpression>> exp) { this.InternalFrom(exp); var ret = new ClickHouseSelect(_orm, _commonUtils, _commonExpression, null); ClickHouseSelect.CopyData(this, ret, exp?.Parameters); return ret; }
+ public override ISelect From(Expression, T2, T3, T4, T5, T6, T7, T8, T9, ISelectFromExpression>> exp) { this.InternalFrom(exp); var ret = new ClickHouseSelect(_orm, _commonUtils, _commonExpression, null); ClickHouseSelect.CopyData(this, ret, exp?.Parameters); return ret; }
+ public override ISelect From(Expression, T2, T3, T4, T5, T6, T7, T8, T9, T10, ISelectFromExpression>> exp) { this.InternalFrom(exp); var ret = new ClickHouseSelect(_orm, _commonUtils, _commonExpression, null); ClickHouseSelect.CopyData(this, ret, exp?.Parameters); return ret; }
+
+ public override ISelect From(Expression, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, ISelectFromExpression>> exp) { this.InternalFrom(exp); var ret = new ClickHouseSelect(_orm, _commonUtils, _commonExpression, null); ClickHouseSelect.CopyData(this, ret, exp?.Parameters); return ret; }
+ public override ISelect From(Expression, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, ISelectFromExpression>> exp) { this.InternalFrom(exp); var ret = new ClickHouseSelect(_orm, _commonUtils, _commonExpression, null); ClickHouseSelect.CopyData(this, ret, exp?.Parameters); return ret; }
+ public override ISelect From(Expression, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, ISelectFromExpression>> exp) { this.InternalFrom(exp); var ret = new ClickHouseSelect(_orm, _commonUtils, _commonExpression, null); ClickHouseSelect.CopyData(this, ret, exp?.Parameters); return ret; }
+ public override ISelect From(Expression, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, ISelectFromExpression>> exp) { this.InternalFrom(exp); var ret = new ClickHouseSelect(_orm, _commonUtils, _commonExpression, null); ClickHouseSelect.CopyData(this, ret, exp?.Parameters); return ret; }
+ public override ISelect From(Expression, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, ISelectFromExpression>> exp) { this.InternalFrom(exp); var ret = new ClickHouseSelect(_orm, _commonUtils, _commonExpression, null); ClickHouseSelect.CopyData(this, ret, exp?.Parameters); return ret; }
+ public override ISelect From(Expression, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, ISelectFromExpression>> exp) { this.InternalFrom(exp); var ret = new ClickHouseSelect(_orm, _commonUtils, _commonExpression, null); ClickHouseSelect