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.CopyData(this, ret, exp?.Parameters); return ret; } + public override string ToSql(string field = null) => ToSqlStatic(_commonUtils, _commonExpression, _select, _distinct, field ?? this.GetAllFieldExpressionTreeLevel2().Field, _join, _where, _groupby, _having, _orderby, _skip, _limit, _tables, this.GetTableRuleUnions(), _aliasRule, _tosqlAppendContent, _whereGlobalFilter, _orm); + } + class ClickHouseSelect : FreeSql.Internal.CommonProvider.Select2Provider where T2 : class + { + public ClickHouseSelect(IFreeSql orm, CommonUtils commonUtils, CommonExpression commonExpression, object dywhere) : base(orm, commonUtils, commonExpression, dywhere) { } + public override string ToSql(string field = null) => ClickHouseSelect.ToSqlStatic(_commonUtils, _commonExpression, _select, _distinct, field ?? this.GetAllFieldExpressionTreeLevel2().Field, _join, _where, _groupby, _having, _orderby, _skip, _limit, _tables, this.GetTableRuleUnions(), _aliasRule, _tosqlAppendContent, _whereGlobalFilter, _orm); + } + class ClickHouseSelect : FreeSql.Internal.CommonProvider.Select3Provider where T2 : class where T3 : class + { + public ClickHouseSelect(IFreeSql orm, CommonUtils commonUtils, CommonExpression commonExpression, object dywhere) : base(orm, commonUtils, commonExpression, dywhere) { } + public override string ToSql(string field = null) => ClickHouseSelect.ToSqlStatic(_commonUtils, _commonExpression, _select, _distinct, field ?? this.GetAllFieldExpressionTreeLevel2().Field, _join, _where, _groupby, _having, _orderby, _skip, _limit, _tables, this.GetTableRuleUnions(), _aliasRule, _tosqlAppendContent, _whereGlobalFilter, _orm); + } + class ClickHouseSelect : FreeSql.Internal.CommonProvider.Select4Provider where T2 : class where T3 : class where T4 : class + { + public ClickHouseSelect(IFreeSql orm, CommonUtils commonUtils, CommonExpression commonExpression, object dywhere) : base(orm, commonUtils, commonExpression, dywhere) { } + public override string ToSql(string field = null) => ClickHouseSelect.ToSqlStatic(_commonUtils, _commonExpression, _select, _distinct, field ?? this.GetAllFieldExpressionTreeLevel2().Field, _join, _where, _groupby, _having, _orderby, _skip, _limit, _tables, this.GetTableRuleUnions(), _aliasRule, _tosqlAppendContent, _whereGlobalFilter, _orm); + } + class ClickHouseSelect : FreeSql.Internal.CommonProvider.Select5Provider where T2 : class where T3 : class where T4 : class where T5 : class + { + public ClickHouseSelect(IFreeSql orm, CommonUtils commonUtils, CommonExpression commonExpression, object dywhere) : base(orm, commonUtils, commonExpression, dywhere) { } + public override string ToSql(string field = null) => ClickHouseSelect.ToSqlStatic(_commonUtils, _commonExpression, _select, _distinct, field ?? this.GetAllFieldExpressionTreeLevel2().Field, _join, _where, _groupby, _having, _orderby, _skip, _limit, _tables, this.GetTableRuleUnions(), _aliasRule, _tosqlAppendContent, _whereGlobalFilter, _orm); + } + class ClickHouseSelect : FreeSql.Internal.CommonProvider.Select6Provider where T2 : class where T3 : class where T4 : class where T5 : class where T6 : class + { + public ClickHouseSelect(IFreeSql orm, CommonUtils commonUtils, CommonExpression commonExpression, object dywhere) : base(orm, commonUtils, commonExpression, dywhere) { } + public override string ToSql(string field = null) => ClickHouseSelect.ToSqlStatic(_commonUtils, _commonExpression, _select, _distinct, field ?? this.GetAllFieldExpressionTreeLevel2().Field, _join, _where, _groupby, _having, _orderby, _skip, _limit, _tables, this.GetTableRuleUnions(), _aliasRule, _tosqlAppendContent, _whereGlobalFilter, _orm); + } + class ClickHouseSelect : FreeSql.Internal.CommonProvider.Select7Provider where T2 : class where T3 : class where T4 : class where T5 : class where T6 : class where T7 : class + { + public ClickHouseSelect(IFreeSql orm, CommonUtils commonUtils, CommonExpression commonExpression, object dywhere) : base(orm, commonUtils, commonExpression, dywhere) { } + public override string ToSql(string field = null) => ClickHouseSelect.ToSqlStatic(_commonUtils, _commonExpression, _select, _distinct, field ?? this.GetAllFieldExpressionTreeLevel2().Field, _join, _where, _groupby, _having, _orderby, _skip, _limit, _tables, this.GetTableRuleUnions(), _aliasRule, _tosqlAppendContent, _whereGlobalFilter, _orm); + } + class ClickHouseSelect : FreeSql.Internal.CommonProvider.Select8Provider where T2 : class where T3 : class where T4 : class where T5 : class where T6 : class where T7 : class where T8 : class + { + public ClickHouseSelect(IFreeSql orm, CommonUtils commonUtils, CommonExpression commonExpression, object dywhere) : base(orm, commonUtils, commonExpression, dywhere) { } + public override string ToSql(string field = null) => ClickHouseSelect.ToSqlStatic(_commonUtils, _commonExpression, _select, _distinct, field ?? this.GetAllFieldExpressionTreeLevel2().Field, _join, _where, _groupby, _having, _orderby, _skip, _limit, _tables, this.GetTableRuleUnions(), _aliasRule, _tosqlAppendContent, _whereGlobalFilter, _orm); + } + class ClickHouseSelect : FreeSql.Internal.CommonProvider.Select9Provider where T2 : class where T3 : class where T4 : class where T5 : class where T6 : class where T7 : class where T8 : class where T9 : class + { + public ClickHouseSelect(IFreeSql orm, CommonUtils commonUtils, CommonExpression commonExpression, object dywhere) : base(orm, commonUtils, commonExpression, dywhere) { } + public override string ToSql(string field = null) => ClickHouseSelect.ToSqlStatic(_commonUtils, _commonExpression, _select, _distinct, field ?? this.GetAllFieldExpressionTreeLevel2().Field, _join, _where, _groupby, _having, _orderby, _skip, _limit, _tables, this.GetTableRuleUnions(), _aliasRule, _tosqlAppendContent, _whereGlobalFilter, _orm); + } + class ClickHouseSelect : FreeSql.Internal.CommonProvider.Select10Provider where T2 : class where T3 : class where T4 : class where T5 : class where T6 : class where T7 : class where T8 : class where T9 : class where T10 : class + { + public ClickHouseSelect(IFreeSql orm, CommonUtils commonUtils, CommonExpression commonExpression, object dywhere) : base(orm, commonUtils, commonExpression, dywhere) { } + public override string ToSql(string field = null) => ClickHouseSelect.ToSqlStatic(_commonUtils, _commonExpression, _select, _distinct, field ?? this.GetAllFieldExpressionTreeLevel2().Field, _join, _where, _groupby, _having, _orderby, _skip, _limit, _tables, this.GetTableRuleUnions(), _aliasRule, _tosqlAppendContent, _whereGlobalFilter, _orm); + } + + class ClickHouseSelect : FreeSql.Internal.CommonProvider.Select11Provider where T2 : class where T3 : class where T4 : class where T5 : class where T6 : class where T7 : class where T8 : class where T9 : class where T10 : class where T11 : class + { + public ClickHouseSelect(IFreeSql orm, CommonUtils commonUtils, CommonExpression commonExpression, object dywhere) : base(orm, commonUtils, commonExpression, dywhere) { } + public override string ToSql(string field = null) => ClickHouseSelect.ToSqlStatic(_commonUtils, _commonExpression, _select, _distinct, field ?? this.GetAllFieldExpressionTreeLevel2().Field, _join, _where, _groupby, _having, _orderby, _skip, _limit, _tables, this.GetTableRuleUnions(), _aliasRule, _tosqlAppendContent, _whereGlobalFilter, _orm); + } + class ClickHouseSelect : FreeSql.Internal.CommonProvider.Select12Provider where T2 : class where T3 : class where T4 : class where T5 : class where T6 : class where T7 : class where T8 : class where T9 : class where T10 : class where T11 : class where T12 : class + { + public ClickHouseSelect(IFreeSql orm, CommonUtils commonUtils, CommonExpression commonExpression, object dywhere) : base(orm, commonUtils, commonExpression, dywhere) { } + public override string ToSql(string field = null) => ClickHouseSelect.ToSqlStatic(_commonUtils, _commonExpression, _select, _distinct, field ?? this.GetAllFieldExpressionTreeLevel2().Field, _join, _where, _groupby, _having, _orderby, _skip, _limit, _tables, this.GetTableRuleUnions(), _aliasRule, _tosqlAppendContent, _whereGlobalFilter, _orm); + } + class ClickHouseSelect : FreeSql.Internal.CommonProvider.Select13Provider where T2 : class where T3 : class where T4 : class where T5 : class where T6 : class where T7 : class where T8 : class where T9 : class where T10 : class where T11 : class where T12 : class where T13 : class + { + public ClickHouseSelect(IFreeSql orm, CommonUtils commonUtils, CommonExpression commonExpression, object dywhere) : base(orm, commonUtils, commonExpression, dywhere) { } + public override string ToSql(string field = null) => ClickHouseSelect.ToSqlStatic(_commonUtils, _commonExpression, _select, _distinct, field ?? this.GetAllFieldExpressionTreeLevel2().Field, _join, _where, _groupby, _having, _orderby, _skip, _limit, _tables, this.GetTableRuleUnions(), _aliasRule, _tosqlAppendContent, _whereGlobalFilter, _orm); + } + class ClickHouseSelect : FreeSql.Internal.CommonProvider.Select14Provider where T2 : class where T3 : class where T4 : class where T5 : class where T6 : class where T7 : class where T8 : class where T9 : class where T10 : class where T11 : class where T12 : class where T13 : class where T14 : class + { + public ClickHouseSelect(IFreeSql orm, CommonUtils commonUtils, CommonExpression commonExpression, object dywhere) : base(orm, commonUtils, commonExpression, dywhere) { } + public override string ToSql(string field = null) => ClickHouseSelect.ToSqlStatic(_commonUtils, _commonExpression, _select, _distinct, field ?? this.GetAllFieldExpressionTreeLevel2().Field, _join, _where, _groupby, _having, _orderby, _skip, _limit, _tables, this.GetTableRuleUnions(), _aliasRule, _tosqlAppendContent, _whereGlobalFilter, _orm); + } + class ClickHouseSelect : FreeSql.Internal.CommonProvider.Select15Provider where T2 : class where T3 : class where T4 : class where T5 : class where T6 : class where T7 : class where T8 : class where T9 : class where T10 : class where T11 : class where T12 : class where T13 : class where T14 : class where T15 : class + { + public ClickHouseSelect(IFreeSql orm, CommonUtils commonUtils, CommonExpression commonExpression, object dywhere) : base(orm, commonUtils, commonExpression, dywhere) { } + public override string ToSql(string field = null) => ClickHouseSelect.ToSqlStatic(_commonUtils, _commonExpression, _select, _distinct, field ?? this.GetAllFieldExpressionTreeLevel2().Field, _join, _where, _groupby, _having, _orderby, _skip, _limit, _tables, this.GetTableRuleUnions(), _aliasRule, _tosqlAppendContent, _whereGlobalFilter, _orm); + } + class ClickHouseSelect : FreeSql.Internal.CommonProvider.Select16Provider where T2 : class where T3 : class where T4 : class where T5 : class where T6 : class where T7 : class where T8 : class where T9 : class where T10 : class where T11 : class where T12 : class where T13 : class where T14 : class where T15 : class where T16 : class + { + public ClickHouseSelect(IFreeSql orm, CommonUtils commonUtils, CommonExpression commonExpression, object dywhere) : base(orm, commonUtils, commonExpression, dywhere) { } + public override string ToSql(string field = null) => ClickHouseSelect.ToSqlStatic(_commonUtils, _commonExpression, _select, _distinct, field ?? this.GetAllFieldExpressionTreeLevel2().Field, _join, _where, _groupby, _having, _orderby, _skip, _limit, _tables, this.GetTableRuleUnions(), _aliasRule, _tosqlAppendContent, _whereGlobalFilter, _orm); + } +} diff --git a/Providers/FreeSql.Provider.ClickHouse/Curd/ClickHouseUpdate.cs b/Providers/FreeSql.Provider.ClickHouse/Curd/ClickHouseUpdate.cs new file mode 100644 index 00000000..9e69b3c9 --- /dev/null +++ b/Providers/FreeSql.Provider.ClickHouse/Curd/ClickHouseUpdate.cs @@ -0,0 +1,381 @@ +using FreeSql.Internal; +using FreeSql.Internal.Model; +using System; +using System.Collections.Generic; +using System.Data; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace FreeSql.ClickHouse.Curd +{ + + class ClickHouseUpdate : Internal.CommonProvider.UpdateProvider + { + + public ClickHouseUpdate(IFreeSql orm, CommonUtils commonUtils, CommonExpression commonExpression, object dywhere) + : base(orm, commonUtils, commonExpression, dywhere) + { + } + + internal StringBuilder InternalSbSet => _set; + internal StringBuilder InternalSbSetIncr => _setIncr; + internal Dictionary InternalIgnore => _ignore; + internal void InternalResetSource(List source) => _source = source; + internal string InternalWhereCaseSource(string CsName, Func thenValue) => WhereCaseSource(CsName, thenValue); + internal void InternalToSqlCaseWhenEnd(StringBuilder sb, ColumnInfo col) => ToSqlCaseWhenEnd(sb, col); + + public override int ExecuteAffrows() => SplitExecuteAffrows(_batchRowsLimit > 0 ? _batchRowsLimit : 500, _batchParameterLimit > 0 ? _batchParameterLimit : 3000); + public override List ExecuteUpdated() => SplitExecuteUpdated(_batchRowsLimit > 0 ? _batchRowsLimit : 500, _batchParameterLimit > 0 ? _batchParameterLimit : 3000); + protected override List RawExecuteUpdated() => throw new NotImplementedException("FreeSql.ClickHouse.Custom 未实现该功能 未实现该功能"); + protected override void ToSqlCase(StringBuilder caseWhen, ColumnInfo[] primarys) + { + if (primarys.Length == 1) + { + var pk = primarys.First(); + caseWhen.Append(_commonUtils.RereadColumn(pk, _commonUtils.QuoteSqlName(pk.Attribute.Name))); + return; + } + caseWhen.Append("CONCAT("); + var pkidx = 0; + foreach (var pk in primarys) + { + if (pkidx > 0) caseWhen.Append(", '+', "); + caseWhen.Append(_commonUtils.RereadColumn(pk, _commonUtils.QuoteSqlName(pk.Attribute.Name))); + ++pkidx; + } + caseWhen.Append(")"); + } + protected override void ToSqlWhen(StringBuilder sb, ColumnInfo[] primarys, object d) + { + if (primarys.Length == 1) + { + sb.Append(_commonUtils.FormatSql("{0}", primarys[0].GetDbValue(d))); + return; + } + sb.Append("concat("); + var pkidx = 0; + foreach (var pk in primarys) + { + if (pkidx > 0) sb.Append(", '+', "); + sb.Append(_commonUtils.FormatSql("{0}", pk.GetDbValue(d))); + ++pkidx; + } + sb.Append(")"); + } + + public override string ToSql() + { + if (_where.Length == 0 && _source.Any() == false) return null; + + var sb = new StringBuilder(); + sb.Append("ALTER TABLE ").Append(_commonUtils.QuoteSqlName(TableRuleInvoke())).Append(" UPDATE "); + + if (_set.Length > 0) + { //指定 set 更新 + sb.Append(_set.ToString().Substring(2)); + + } + else if (_source.Count == 1) + { //保存 Source + _paramsSource.Clear(); + var colidx = 0; + foreach (var col in _table.Columns.Values) + { + if (col.Attribute.IsPrimary) continue; + if (_tempPrimarys.Any(a => a.CsName == col.CsName)) continue; + if (col.Attribute.IsIdentity == false && col.Attribute.IsVersion == false && _ignore.ContainsKey(col.Attribute.Name) == false) + { + if (colidx > 0) sb.Append(", "); + sb.Append(_commonUtils.QuoteSqlName(col.Attribute.Name)).Append(" = "); + + if (col.Attribute.CanUpdate && string.IsNullOrEmpty(col.DbUpdateValue) == false) + sb.Append(col.DbUpdateValue); + else + { + var val = col.GetDbValue(_source.First()); + + var colsql = _noneParameter ? _commonUtils.GetNoneParamaterSqlValue(_paramsSource, "u", col, col.Attribute.MapType, val) : + _commonUtils.QuoteWriteParamterAdapter(col.Attribute.MapType, _commonUtils.QuoteParamterName($"p_{_paramsSource.Count}")); + sb.Append(_commonUtils.RewriteColumn(col, colsql)); + if (_noneParameter == false) + _commonUtils.AppendParamter(_paramsSource, null, col, col.Attribute.MapType, val); + } + ++colidx; + } + } + if (colidx == 0) return null; + + } + else if (_source.Count > 1) + { //批量保存 Source + if (_tempPrimarys.Any() == false) return null; + + var caseWhen = new StringBuilder(); + ToSqlCase(caseWhen, _tempPrimarys); + var cw = $"{caseWhen.ToString()}="; + _paramsSource.Clear(); + var colidx = 0; + foreach (var col in _table.Columns.Values) + { + if (col.Attribute.IsPrimary) continue; + if (_tempPrimarys.Any(a => a.CsName == col.CsName)) continue; + if (col.Attribute.IsIdentity == false && col.Attribute.IsVersion == false && _ignore.ContainsKey(col.Attribute.Name) == false) + { + if (colidx > 0) sb.Append(", "); + var columnName = _commonUtils.QuoteSqlName(col.Attribute.Name); + sb.Append(columnName).Append(" = "); + + if (col.Attribute.CanUpdate && string.IsNullOrEmpty(col.DbUpdateValue) == false) + sb.Append(col.DbUpdateValue); + else + { + var nulls = 0; + var cwsb = new StringBuilder().Append(" multiIf( "); + foreach (var d in _source) + { + cwsb.Append(cw); + ToSqlWhen(cwsb, _tempPrimarys, d); + cwsb.Append(","); + var val = col.GetDbValue(d); + + var colsql = _noneParameter ? _commonUtils.GetNoneParamaterSqlValue(_paramsSource, "u", col, col.Attribute.MapType, val) : + _commonUtils.QuoteWriteParamterAdapter(col.Attribute.MapType, _commonUtils.QuoteParamterName($"p_{_paramsSource.Count}")); + cwsb.Append(_commonUtils.RewriteColumn(col, colsql)); + if (_noneParameter == false) + _commonUtils.AppendParamter(_paramsSource, null, col, col.Attribute.MapType, val); + if (val == null || val == DBNull.Value) nulls++; + cwsb.Append(", "); + } + if (nulls == _source.Count) sb.Append("NULL"); + else + { + cwsb.Append(columnName).Append(" )"); + ToSqlCaseWhenEnd(cwsb, col); + sb.Append(cwsb); + } + cwsb.Clear(); + } + ++colidx; + } + } + if (colidx == 0) return null; + } + else if (_setIncr.Length == 0) + return null; + + if (_setIncr.Length > 0) + sb.Append(_set.Length > 0 ? _setIncr.ToString() : _setIncr.ToString().Substring(2)); + + if (_source.Any() == false) + { + foreach (var col in _table.Columns.Values) + if (col.Attribute.CanUpdate && string.IsNullOrEmpty(col.DbUpdateValue) == false) + sb.Append(", ").Append(_commonUtils.QuoteSqlName(col.Attribute.Name)).Append(" = ").Append(col.DbUpdateValue); + } + + if (_table.VersionColumn != null) + { + var vcname = _commonUtils.QuoteSqlName(_table.VersionColumn.Attribute.Name); + if (_table.VersionColumn.Attribute.MapType == typeof(byte[])) + { + _updateVersionValue = Utils.GuidToBytes(Guid.NewGuid()); + sb.Append(", ").Append(vcname).Append(" = ").Append(_commonUtils.GetNoneParamaterSqlValue(_paramsSource, "uv", _table.VersionColumn, _table.VersionColumn.Attribute.MapType, _updateVersionValue)); + } + else + sb.Append(", ").Append(vcname).Append(" = ").Append(_commonUtils.IsNull(vcname, 0)).Append(" + 1"); + } + + sb.Append(" \r\nWHERE "); + if (_source.Any()) + { + if (_tempPrimarys.Any() == false) throw new ArgumentException($"{_table.Type.DisplayCsharp()} 没有定义主键,无法使用 SetSource,请尝试 SetDto"); + sb.Append('(').Append(_commonUtils.WhereItems(_tempPrimarys, "", _source)).Append(')'); + } + + if (_where.Length > 0) + sb.Append(_source.Any() ? _where.ToString() : _where.ToString().Substring(5)); + + if (_whereGlobalFilter.Any()) + { + var globalFilterCondi = _commonExpression.GetWhereCascadeSql(new SelectTableInfo { Table = _table }, _whereGlobalFilter, false); + if (string.IsNullOrEmpty(globalFilterCondi) == false) + sb.Append(" AND ").Append(globalFilterCondi); + } + + if (_table.VersionColumn != null) + { + var versionCondi = WhereCaseSource(_table.VersionColumn.CsName, sqlval => sqlval); + if (string.IsNullOrEmpty(versionCondi) == false) + sb.Append(" AND ").Append(versionCondi); + } + + _interceptSql?.Invoke(sb); + return sb.ToString(); + } + + protected override int SplitExecuteAffrows(int valuesLimit, int parameterLimit) + { + var ss = base.SplitSource(valuesLimit, parameterLimit); + var ret = 0; + if (ss.Length <= 1) + { + if (_source?.Any() == true) _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; + } + protected override List SplitExecuteUpdated(int valuesLimit, int parameterLimit) + { + var ss = SplitSource(valuesLimit, parameterLimit); + var ret = new List(); + if (ss.Length <= 1) + { + if (_source?.Any() == true) _batchProgress?.Invoke(new BatchProgressStatus(_source, 1, 1)); + ret = this.RawExecuteUpdated(); + ClearData(); + return ret; + } + + var before = new Aop.TraceBeforeEventArgs("SplitExecuteUpdated", 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(this.RawExecuteUpdated()); + } + } + catch (Exception ex) + { + exception = ex; + throw; + } + finally + { + var after = new Aop.TraceAfterEventArgs(before, null, exception); + _orm.Aop.TraceAfterHandler?.Invoke(this, after); + } + ClearData(); + return ret; + } + + +#if net40 +#else + public override Task ExecuteAffrowsAsync(CancellationToken cancellationToken = default) => SplitExecuteAffrowsAsync(_batchRowsLimit > 0 ? _batchRowsLimit : 500, _batchParameterLimit > 0 ? _batchParameterLimit : 3000, cancellationToken); + public override Task> ExecuteUpdatedAsync(CancellationToken cancellationToken = default) => SplitExecuteUpdatedAsync(_batchRowsLimit > 0 ? _batchRowsLimit : 500, _batchParameterLimit > 0 ? _batchParameterLimit : 3000, cancellationToken); + protected override Task> RawExecuteUpdatedAsync(CancellationToken cancellationToken = default) => throw new NotImplementedException("FreeSql.ClickHouse.Custom 未实现该功能 未实现该功能"); + + async protected override Task SplitExecuteAffrowsAsync(int valuesLimit, int parameterLimit, CancellationToken cancellationToken = default) + + { + var ss = SplitSource(valuesLimit, parameterLimit); + var ret = 0; + if (ss.Length <= 1) + { + if (_source?.Any() == true) _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> SplitExecuteUpdatedAsync(int valuesLimit, int parameterLimit, CancellationToken cancellationToken = default) + { + var ss = SplitSource(valuesLimit, parameterLimit); + var ret = new List(); + if (ss.Length <= 1) + { + if (_source?.Any() == true) _batchProgress?.Invoke(new BatchProgressStatus(_source, 1, 1)); + ret = await this.RawExecuteUpdatedAsync(cancellationToken); + ClearData(); + return ret; + } + + var before = new Aop.TraceBeforeEventArgs("SplitExecuteUpdatedAsync", 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.RawExecuteUpdatedAsync(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; + } + +#endif + } +} diff --git a/Providers/FreeSql.Provider.ClickHouse/FreeSql.Provider.ClickHouse.csproj b/Providers/FreeSql.Provider.ClickHouse/FreeSql.Provider.ClickHouse.csproj new file mode 100644 index 00000000..43b24148 --- /dev/null +++ b/Providers/FreeSql.Provider.ClickHouse/FreeSql.Provider.ClickHouse.csproj @@ -0,0 +1,45 @@ + + + + + netstandard2.1 + 2.6.100 + true + FreeSql;ncc;YeXiangQin;ChenBo + FreeSql 数据库实现,基于 ClickHouse.Client Ado.net + https://github.com/2881099/FreeSql + https://github.com/2881099/FreeSql + git + MIT + FreeSql;ORM + $(AssemblyName) + logo.png + $(AssemblyName) + true + true + False + key.snk + false + + + + + + + + + + + + + 4.0.1.462 + + + + + + + + + +