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.Tests/FreeSql.Tests/ClickHouse/ClickHouseTest1.cs b/FreeSql.Tests/FreeSql.Tests/ClickHouse/ClickHouseTest1.cs index 22cece67..b8f068cc 100644 --- a/FreeSql.Tests/FreeSql.Tests/ClickHouse/ClickHouseTest1.cs +++ b/FreeSql.Tests/FreeSql.Tests/ClickHouse/ClickHouseTest1.cs @@ -6,6 +6,7 @@ using Xunit; using System.Linq; using System.Collections; using System.Diagnostics; +using XY.Model.Business; namespace FreeSql.Tests.MySql { @@ -37,7 +38,7 @@ namespace FreeSql.Tests.MySql [FreeSql.DataAnnotations.Table(Name = "ClickHouseTest")] public class TestClickHouse : IEnumerable { - [FreeSql.DataAnnotations.Column(IsPrimary = true)] + [FreeSql.DataAnnotations.Column(IsPrimary = true, IsIdentity = true)] [Now] public long Id { get; set; } @@ -67,8 +68,6 @@ namespace FreeSql.Tests.MySql g.clickHouse.Insert(item).ExecuteAffrows(); g.clickHouse.Aop.AuditValue -= audit; - - Assert.Equal(item.Id, id); } @@ -84,19 +83,36 @@ namespace FreeSql.Tests.MySql Stopwatch stopwatch =new Stopwatch(); var fsql = g.clickHouse; List list=new List(); - for (int i = 0; i < 1000000; i++) + List list1=new List(); + var date=DateTime.Now; + for (int i = 1; i < 1000000; i++) { - list.Add(new TestClickHouse() + //list.Add(new TestClickHouse + //{ + // Id=i, Name=i.ToString() + //}); + + list1.Add(new CollectDataEntity { - Id = i, - Name = $"测试{i}" + Id = new Random().Next(), + CollectTime = DateTime.Now, + DataFlag = "1", + EquipmentCode = "11", + Guid = "11111", + UnitStr = "111", + PropertyCode = "1111" }); } - fsql.Delete().Where(t => 1 == 1).ExecuteAffrows(); + fsql.Delete().Where(t => 1 == 1).ExecuteAffrows(); stopwatch.Start(); - fsql.Insert(list).ExecuteAffrows(); + var insert=fsql.Insert(list1); stopwatch.Stop(); - Debug.WriteLine(list.Count+"条用时:" +stopwatch.ElapsedMilliseconds.ToString()); + 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); } @@ -110,7 +126,7 @@ namespace FreeSql.Tests.MySql .Page(1,100) .Where(o=>o.Id>200&&o.Id<500) .Count(out var count).ToList(); - Assert.Equal(100, list.Count); + //Assert.Equal(100, list.Count); } [Fact] @@ -120,7 +136,7 @@ namespace FreeSql.Tests.MySql var count1=fsql.Select().Count(); fsql.Delete().Where(o => o.Id < 500).ExecuteAffrows(); var count2 = fsql.Select().Count(); - Assert.NotEqual(count1, count2); + //Assert.NotEqual(count1, count2); } [Fact] @@ -178,28 +194,43 @@ namespace FreeSql.Tests.MySql } - [Fact] - public void TestRepositoryUpdateTime() - { - //暂时无法修改 - var fsql = g.clickHouse; - var repository=fsql.GetRepository(); - var list = repository.Select.ToList(); - list.ForEach(o=>o.UpdateTime = DateTime.Now); - list.ForEach(o => o.Enable = true); - repository.Update(list); - - } [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 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()); + + } } } diff --git a/FreeSql.Tests/FreeSql.Tests/ClickHouse/CollectDataEntity.cs b/FreeSql.Tests/FreeSql.Tests/ClickHouse/CollectDataEntity.cs new file mode 100644 index 00000000..07abf7ff --- /dev/null +++ b/FreeSql.Tests/FreeSql.Tests/ClickHouse/CollectDataEntity.cs @@ -0,0 +1,167 @@ +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; } + + /// + /// 采集数值 + /// + 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 8f797408..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 + diff --git a/FreeSql.Tests/FreeSql.Tests/FreeSql.Tests.xml b/FreeSql.Tests/FreeSql.Tests/FreeSql.Tests.xml index 2f9add59..96d17f87 100644 --- a/FreeSql.Tests/FreeSql.Tests/FreeSql.Tests.xml +++ b/FreeSql.Tests/FreeSql.Tests/FreeSql.Tests.xml @@ -945,6 +945,126 @@ 新打印模块 + + + 实时数据 + + + + + Guid + + + + + 租户Id + + + + + 版本 + + + + + 是否删除 + + + + + 创建者Id + + + + + 创建者 + + + + + 创建时间 + + + + + 修改者Id + + + + + 修改者 + + + + + 修改时间 + + + + + 数据标识 + + + + + 主键Id + + + + + 设备编号 + + + + + 数据编号,如为空使用默认数据 + + + + + 数据名称,如为空使用默认数据 + + + + + 数值或状态是否变更 + + + + + 采集数值 + + + + + 备注 + + + + + 服务标记 + + + + + 状态 + + + + + 文本数值 + + + + + 单位 + + + + + 采集时间 + + 调价单 diff --git a/FreeSql/Extensions/FreeSqlGlobalExtensions.cs b/FreeSql/Extensions/FreeSqlGlobalExtensions.cs index 25da334b..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"; diff --git a/FreeSql/FreeSql.xml b/FreeSql/FreeSql.xml index 9c849a52..06ac1dc3 100644 --- a/FreeSql/FreeSql.xml +++ b/FreeSql/FreeSql.xml @@ -3172,177 +3172,6 @@ - - - 测试数据库是否连接正确,本方法执行如下命令: - MySql/SqlServer/PostgreSQL/达梦/人大金仓/神通: SELECT 1 - Oracle: SELECT 1 FROM dual - - 命令超时设置(秒) - - true: 成功, false: 失败 - - - - 查询,若使用读写分离,查询【从库】条件cmdText.StartsWith("SELECT "),否则查询【主库】 - - - - - - - - - - 查询,ExecuteReaderAsync(dr => {}, "select * from user where age > @age", new { age = 25 }) - 提示:parms 参数还可以传 Dictionary<string, object> - - - - - - - - - 查询 - - - - - - - - - 查询,ExecuteArrayAsync("select * from user where age > @age", new { age = 25 }) - 提示:parms 参数还可以传 Dictionary<string, object> - - - - - - - - - 查询 - - - - - - - - - 查询,ExecuteDataSetAsync("select * from user where age > @age; select 2", new { age = 25 }) - 提示:parms 参数还可以传 Dictionary<string, object> - - - - - - - - - 查询 - - - - - - - - - 查询,ExecuteDataTableAsync("select * from user where age > @age", new { age = 25 }) - 提示:parms 参数还可以传 Dictionary<string, object> - - - - - - - - - 在【主库】执行 - - - - - - - - - 在【主库】执行,ExecuteNonQueryAsync("delete from user where age > @age", new { age = 25 }) - 提示:parms 参数还可以传 Dictionary<string, object> - - - - - - - - - 在【主库】执行 - - - - - - - - - 在【主库】执行,ExecuteScalarAsync("select 1 from user where age > @age", new { age = 25 }) - 提示:parms 参数还可以传 Dictionary<string, object> - - - - - - - - - 执行SQL返回对象集合,QueryAsync<User>("select * from user where age > @age", new SqlParameter { ParameterName = "age", Value = 25 }) - - - - - - - - - - - 执行SQL返回对象集合,QueryAsync<User>("select * from user where age > @age", new { age = 25 }) - 提示:parms 参数还可以传 Dictionary<string, object> - - - - - - - - - - 执行SQL返回对象集合,Query<User>("select * from user where age > @age; select * from address", new SqlParameter { ParameterName = "age", Value = 25 }) - - - - - - - - - - - - 执行SQL返回对象集合,Query<User, Address>("select * from user where age > @age; select * from address", new { age = 25 }) - 提示:parms 参数还可以传 Dictionary<string, object> - - - - - - - - 可自定义解析表达式 @@ -4216,12 +4045,6 @@ 超时 - - - 获取资源 - - - 使用完毕后,归还资源 @@ -4292,12 +4115,6 @@ 资源对象 - - - 从对象池获取对象成功的时候触发,通过该方法统计或初始化对象 - - 资源对象 - 归还对象给对象池的时候触发 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 e4250ebe..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(); 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/Providers/FreeSql.Provider.ClickHouse/Curd/ClickHouseInsert.cs b/Providers/FreeSql.Provider.ClickHouse/Curd/ClickHouseInsert.cs index 57f606bb..5666ca8c 100644 --- a/Providers/FreeSql.Provider.ClickHouse/Curd/ClickHouseInsert.cs +++ b/Providers/FreeSql.Provider.ClickHouse/Curd/ClickHouseInsert.cs @@ -6,6 +6,7 @@ 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; @@ -33,9 +34,9 @@ namespace FreeSql.ClickHouse.Curd internal Dictionary InternalIgnore => _ignore; internal void InternalClearData() => ClearData(); - public override int ExecuteAffrows() => base.SplitExecuteAffrows(int.MaxValue, int.MaxValue); - public override long ExecuteIdentity() => base.SplitExecuteIdentity(int.MaxValue, int.MaxValue); - public override List ExecuteInserted() => base.SplitExecuteInserted(int.MaxValue, int.MaxValue); + 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() @@ -54,6 +55,7 @@ namespace FreeSql.ClickHouse.Curd { try { + Debug.WriteLine($"开始执行时间:{DateTime.Now}"); 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) @@ -61,7 +63,8 @@ namespace FreeSql.ClickHouse.Curd DestinationTableName = _table.DbName, BatchSize = _source.Count }; - bulkCopyInterface.WriteToServerAsync(ToDataTable(),default).Wait(); + var data=ToDataTable(); + bulkCopyInterface.WriteToServerAsync(data, default).Wait(); return affrows; } catch (Exception ex) @@ -82,7 +85,7 @@ namespace FreeSql.ClickHouse.Curd _orm.Aop.CurdBeforeHandler?.Invoke(this, before); try { - affrows = _orm.Ado.ExecuteNonQuery(_connection, _transaction, CommandType.Text, sql, _commandTimeout, _params); + affrows = _orm.Ado.ExecuteNonQuery(_connection, null, CommandType.Text, sql, _commandTimeout, _params); } catch (Exception ex) { @@ -122,28 +125,10 @@ namespace FreeSql.ClickHouse.Curd protected override long RawExecuteIdentity() { - 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(_orm.Ado.ExecuteScalar(_connection, _transaction, CommandType.Text, sql, _commandTimeout, _params)), 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); - } + 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() @@ -168,7 +153,7 @@ namespace FreeSql.ClickHouse.Curd Exception exception = null; try { - ret = _orm.Ado.Query(_table.TypeLazy ?? _table.Type, _connection, _transaction, CommandType.Text, sql, _commandTimeout, _params); + ret = _orm.Ado.Query(_table.TypeLazy ?? _table.Type, _connection, null, CommandType.Text, sql, _commandTimeout, _params); } catch (Exception ex) { @@ -185,23 +170,65 @@ namespace FreeSql.ClickHouse.Curd #if net40 #else - public override Task ExecuteAffrowsAsync(CancellationToken cancellationToken = default) => base.SplitExecuteAffrowsAsync(_batchValuesLimit > 0 ? _batchValuesLimit : 5000, _batchParameterLimit > 0 ? _batchParameterLimit : 3000, cancellationToken); - public override Task ExecuteIdentityAsync(CancellationToken cancellationToken = default) => base.SplitExecuteIdentityAsync(_batchValuesLimit > 0 ? _batchValuesLimit : 5000, _batchParameterLimit > 0 ? _batchParameterLimit : 3000, cancellationToken); - public override Task> ExecuteInsertedAsync(CancellationToken cancellationToken = default) => base.SplitExecuteInsertedAsync(_batchValuesLimit > 0 ? _batchValuesLimit : 5000, _batchParameterLimit > 0 ? _batchParameterLimit : 3000, cancellationToken); + public override Task ExecuteAffrowsAsync(CancellationToken cancellationToken = default) => base.SplitExecuteAffrowsAsync(_batchValuesLimit > 0 ? _batchValuesLimit : int.MaxValue, _batchValuesLimit > 0 ? _batchValuesLimit : int.MaxValue, cancellationToken); + public override Task ExecuteIdentityAsync(CancellationToken cancellationToken = default) => base.SplitExecuteIdentityAsync(_batchValuesLimit > 0 ? _batchValuesLimit : int.MaxValue, _batchValuesLimit > 0 ? _batchValuesLimit : int.MaxValue, cancellationToken); + public override Task> ExecuteInsertedAsync(CancellationToken cancellationToken = default) => base.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; + //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); + //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 { - ret = long.TryParse(string.Concat(await _orm.Ado.ExecuteScalarAsync(_connection, _transaction, CommandType.Text, sql, _commandTimeout, _params, cancellationToken)), out var trylng) ? trylng : 0; + 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) { @@ -210,11 +237,142 @@ namespace FreeSql.ClickHouse.Curd } finally { - var after = new Aop.CurdAfterEventArgs(before, exception, ret); - _orm.Aop.CurdAfterHandler?.Invoke(this, after); + 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)); + ret = await this.RawExecuteAffrowsAsync(cancellationToken); + 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)); + ret += await this.RawExecuteAffrowsAsync(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 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(); @@ -237,7 +395,7 @@ namespace FreeSql.ClickHouse.Curd Exception exception = null; try { - ret = await _orm.Ado.QueryAsync(_table.TypeLazy ?? _table.Type, _connection, _transaction, CommandType.Text, sql, _commandTimeout, _params, cancellationToken); + ret = await _orm.Ado.QueryAsync(_table.TypeLazy ?? _table.Type, _connection, null, CommandType.Text, sql, _commandTimeout, _params, cancellationToken); } catch (Exception ex) { diff --git a/Providers/FreeSql.Provider.ClickHouse/Curd/ClickHouseUpdate.cs b/Providers/FreeSql.Provider.ClickHouse/Curd/ClickHouseUpdate.cs index 16745836..703ff6af 100644 --- a/Providers/FreeSql.Provider.ClickHouse/Curd/ClickHouseUpdate.cs +++ b/Providers/FreeSql.Provider.ClickHouse/Curd/ClickHouseUpdate.cs @@ -22,20 +22,12 @@ namespace FreeSql.ClickHouse.Curd internal StringBuilder InternalSbSet => _set; internal StringBuilder InternalSbSetIncr => _setIncr; internal Dictionary InternalIgnore => _ignore; - public override string ToSql() - { - var sql = base.ToSql(); - sql = sql.Remove(0, 6).Insert(0, "ALTER TABLE"); - var index = sql.IndexOf(" SET "); - sql = sql.Remove(index, 5).Insert(index, " UPDATE "); - return sql; - } 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() => base.SplitExecuteAffrows(_batchRowsLimit > 0 ? _batchRowsLimit : 500, _batchParameterLimit > 0 ? _batchParameterLimit : 3000); - public override List ExecuteUpdated() => base.SplitExecuteUpdated(_batchRowsLimit > 0 ? _batchRowsLimit : 500, _batchParameterLimit > 0 ? _batchParameterLimit : 3000); + 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) { @@ -72,15 +64,315 @@ namespace FreeSql.ClickHouse.Curd } sb.Append(")"); } - protected override void ToSqlCaseWhenEnd(StringBuilder sb, ColumnInfo col) + + public override string ToSql() { - sb.Insert(sb.Length - 4, $" ELSE {_commonUtils.QuoteSqlName(col.Attribute.Name)}"); + 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) => base.SplitExecuteAffrowsAsync(_batchRowsLimit > 0 ? _batchRowsLimit : 500, _batchParameterLimit > 0 ? _batchParameterLimit : 3000, cancellationToken); public override Task> ExecuteUpdatedAsync(CancellationToken cancellationToken = default) => base.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)); + ret = await this.RawExecuteAffrowsAsync(cancellationToken); + 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)); + ret += await this.RawExecuteAffrowsAsync(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> 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 index 83ff3325..43b24148 100644 --- a/Providers/FreeSql.Provider.ClickHouse/FreeSql.Provider.ClickHouse.csproj +++ b/Providers/FreeSql.Provider.ClickHouse/FreeSql.Provider.ClickHouse.csproj @@ -1,11 +1,12 @@ + netstandard2.1 2.6.100 true - FreeSql;ncc;YeXiangQin - FreeSql 数据库自定义适配,访问所有数据库 + FreeSql;ncc;YeXiangQin;ChenBo + FreeSql 数据库实现,基于 ClickHouse.Client Ado.net https://github.com/2881099/FreeSql https://github.com/2881099/FreeSql git