From be1a1d2b775d4fa2ef4bb799cbe147724721cd6c Mon Sep 17 00:00:00 2001 From: mk3008 Date: Mon, 13 May 2024 23:24:46 +0900 Subject: [PATCH 01/23] Single-table type-safe query build Supports column selection. Supports column aliases. Supports variable selection. Supports Colaesce function. --- .../Carbunql.TypeSafe.csproj | 13 + src/Carbunql.TypeSafe/ExpressionReader.cs | 363 ++++++++++++++++++ src/Carbunql.TypeSafe/Sql.cs | 282 ++++++++++++++ src/Carbunql.sln | 14 +- src/Carbunql/Building/SelectQueryExtension.cs | 7 + src/Carbunql/Clauses/TableDefinitionClause.cs | 4 + src/Carbunql/DbmsConfiguration.cs | 2 +- .../Carbunql.TypeSafe.Test.csproj | 28 ++ .../Carbunql.TypeSafe.Test/SingleTableTest.cs | 184 +++++++++ 9 files changed, 895 insertions(+), 2 deletions(-) create mode 100644 src/Carbunql.TypeSafe/Carbunql.TypeSafe.csproj create mode 100644 src/Carbunql.TypeSafe/ExpressionReader.cs create mode 100644 src/Carbunql.TypeSafe/Sql.cs create mode 100644 test/Carbunql.TypeSafe.Test/Carbunql.TypeSafe.Test.csproj create mode 100644 test/Carbunql.TypeSafe.Test/SingleTableTest.cs diff --git a/src/Carbunql.TypeSafe/Carbunql.TypeSafe.csproj b/src/Carbunql.TypeSafe/Carbunql.TypeSafe.csproj new file mode 100644 index 00000000..97f4473a --- /dev/null +++ b/src/Carbunql.TypeSafe/Carbunql.TypeSafe.csproj @@ -0,0 +1,13 @@ + + + + net6.0 + enable + enable + + + + + + + diff --git a/src/Carbunql.TypeSafe/ExpressionReader.cs b/src/Carbunql.TypeSafe/ExpressionReader.cs new file mode 100644 index 00000000..e4b511c4 --- /dev/null +++ b/src/Carbunql.TypeSafe/ExpressionReader.cs @@ -0,0 +1,363 @@ +using Carbunql.Extensions; +using System.Linq.Expressions; +using System.Reflection; +using System.Text; +using System.Text.RegularExpressions; + +namespace Carbunql.TypeSafe; + +public static class ExpressionReader +{ + public static IEnumerable GetExpressions(this Expression exp) + { + yield return exp; + + if (exp is MethodCallExpression mc) + { + if (mc.Object != null) + { + foreach (var item in mc.Object.GetExpressions()) yield return item; + } + foreach (var arg in mc.Arguments) + { + foreach (var item in arg.GetExpressions()) yield return item; + } + } + else if (exp is UnaryExpression u) + { + foreach (var item in u.Operand.GetExpressions()) yield return item; + } + else if (exp is LambdaExpression l) + { + foreach (var item in l.Body.GetExpressions()) yield return item; + foreach (var prm in l.Parameters) + { + foreach (var item in prm.GetExpressions()) yield return item; + } + } + else if (exp is MemberExpression m) + { + if (m.Expression != null) + { + foreach (var item in m.Expression.GetExpressions()) yield return item; + } + } + else if (exp is NewExpression n) + { + foreach (var arg in n.Arguments) + { + foreach (var item in arg.GetExpressions()) yield return item; + } + } + } + + public static string Analyze(Expression exp) + { + if (exp is MethodCallExpression m) + { + return Analyze(m); + } + else if (exp is ConstantExpression c) + { + return Analyze(c); + } + else if (exp is UnaryExpression u) + { + return Analyze(u); + } + else if (exp is LambdaExpression l) + { + return Analyze(l); + } + else if (exp is MemberExpression mem) + { + return Analyze(mem); + } + else if (exp is ParameterExpression p) + { + return Analyze(p); + } + else if (exp is MemberInitExpression mi) + { + return Analyze(mi); + } + else if (exp is NewExpression ne) + { + return Analyze(ne); + } + else if (exp is BinaryExpression be) + { + return Analyze(be); + } + return $"not support : {exp.NodeType}"; + } + + internal static string Analyze(MethodCallExpression exp) + { + var sb = new StringBuilder(); + + sb.AppendLine($"* MethodCallExpression"); + sb.AppendLine($"NodeType\r\n {exp.NodeType}"); + sb.AppendLine($"MethodName\r\n {exp.Method.Name}"); + sb.AppendLine($"Type\r\n {exp.Type.Name}"); + + sb.AppendLine($"Object"); + if (exp.Object != null) + { + sb.Append(Analyze(exp.Object).InsertIndent()); + } + else + { + sb.AppendLine($" [NULL]"); + } + + var cnt = exp.Arguments.Count; + sb.AppendLine($"Arguments.Count\r\n {cnt}"); + + foreach (var arg in exp.Arguments) + { + sb.AppendLine($"- index : {exp.Arguments.IndexOf(arg)}"); + sb.AppendLine(Analyze(arg).InsertIndent()); + } + + return sb.ToString().RemoveLastReturn(); + } + + internal static string Analyze(ConstantExpression exp) + { + var sb = new StringBuilder(); + + sb.AppendLine($"* ConstantExpression"); + sb.AppendLine($"NodeType\r\n {exp.NodeType}"); + sb.AppendLine($"Type\r\n {exp.Type.Name}"); + + if (exp.Value != null) + { + sb.AppendLine($"Value\r\n {exp.Value.ToString()}"); + } + else + { + sb.AppendLine($"Value\r\n [NULL]"); + } + return sb.ToString().RemoveLastReturn(); + } + + internal static string Analyze(BinaryExpression exp) + { + var sb = new StringBuilder(); + + sb.AppendLine($"* BinaryExpression"); + sb.AppendLine($"NodeType\r\n {exp.NodeType}"); + sb.AppendLine($"Type\r\n {exp.Type.Name}"); + + if (exp.Method != null) + { + sb.AppendLine($"Method"); + sb.AppendLine(Analyze(exp.Method).InsertIndent()); + } + else + { + sb.AppendLine($"Method\r\n [NULL]"); + } + + sb.AppendLine($"Left"); + sb.AppendLine(Analyze(exp.Left).InsertIndent()); + + sb.AppendLine($"Right"); + sb.AppendLine(Analyze(exp.Right).InsertIndent()); + + + return sb.ToString().RemoveLastReturn(); + } + + internal static string Analyze(UnaryExpression exp) + { + var sb = new StringBuilder(); + + sb.AppendLine($"* UnaryExpression"); + sb.AppendLine($"NodeType\r\n {exp.NodeType}"); + sb.AppendLine($"Type\r\n {exp.Type.Name}"); + + if (exp.Method != null) + { + sb.AppendLine($"Method"); + sb.AppendLine(Analyze(exp.Method).InsertIndent()); + } + else + { + sb.AppendLine($"Method\r\n [NULL]"); + } + + sb.AppendLine($"Operand"); + sb.AppendLine(Analyze(exp.Operand).InsertIndent()); + + return sb.ToString().RemoveLastReturn(); + } + + internal static string Analyze(LambdaExpression exp) + { + var sb = new StringBuilder(); + + sb.AppendLine($"* LambdaExpression"); + sb.AppendLine($"NodeType\r\n {exp.NodeType}"); + sb.AppendLine($"Type\r\n {exp.Type.Name}"); + sb.AppendLine($"Name\r\n \"{exp.Name}\""); + + sb.AppendLine($"ReturnType\r\n {exp.ReturnType}"); + sb.AppendLine($"Body"); + sb.AppendLine(Analyze(exp.Body).InsertIndent()); + + sb.AppendLine($"Parameters count\r\n {exp.Parameters.Count}"); + foreach (var arg in exp.Parameters) + { + sb.AppendLine($"- index : {exp.Parameters.IndexOf(arg)}"); + sb.AppendLine(Analyze(arg).InsertIndent()); + } + + return sb.ToString().RemoveLastReturn(); + } + + internal static string Analyze(MemberExpression exp) + { + var sb = new StringBuilder(); + + sb.AppendLine($"* MemberExpression"); + sb.AppendLine($"NodeType\r\n {exp.NodeType}"); + sb.AppendLine($"Type\r\n {exp.Type.Name}"); + sb.AppendLine($"Member"); + sb.AppendLine(Analyze(exp.Member).InsertIndent()); + + if (exp.Expression != null) + { + sb.AppendLine($"Expression"); + sb.AppendLine(Analyze(exp.Expression).InsertIndent()); + } + else + { + sb.AppendLine($"Expression\r\n [NULL]"); + } + + var s = sb.ToString(); + return sb.ToString().RemoveLastReturn(); + } + + internal static string Analyze(ParameterExpression exp) + { + var sb = new StringBuilder(); + + sb.AppendLine($"* ParameterExpression"); + sb.AppendLine($"NodeType\r\n {exp.NodeType}"); + sb.AppendLine($"Type\r\n {exp.Type.Name}"); + sb.AppendLine($"Name\r\n {exp.Name}"); + + return sb.ToString().RemoveLastReturn(); + } + + internal static string Analyze(NewExpression exp) + { + var sb = new StringBuilder(); + + sb.AppendLine($"* NewExpression"); + sb.AppendLine($"NodeType\r\n {exp.NodeType}"); + sb.AppendLine($"Type\r\n {exp.Type.Name}"); + + if (exp.Arguments != null) + { + var cnt = exp.Arguments.Count; + sb.AppendLine($"Arguments.Count\r\n {cnt}"); + + foreach (var arg in exp.Arguments) + { + sb.AppendLine($"- index : {exp.Arguments.IndexOf(arg)}"); + sb.AppendLine(Analyze(arg).InsertIndent()); + } + } + else + { + sb.AppendLine($"Arguments.Count\r\n 0"); + + } + if (exp.Members != null) + { + var cnt = exp.Members.Count; + sb.AppendLine($"Members.Count\r\n {cnt}"); + + foreach (var arg in exp.Members) + { + sb.AppendLine($"- index : {exp.Members.IndexOf(arg)}"); + sb.AppendLine(Analyze(arg).InsertIndent()); + } + } + else + { + sb.AppendLine($"Members.Count\r\n 0"); + } + + return sb.ToString().RemoveLastReturn(); + } + + internal static string Analyze(MemberInitExpression exp) + { + var sb = new StringBuilder(); + + sb.AppendLine($"* MemberInitExpression"); + sb.AppendLine($"NodeType\r\n {exp.NodeType}"); + sb.AppendLine($"Type\r\n {exp.Type.Name}"); + + if (exp.NewExpression != null) + { + sb.AppendLine($"NewExpression"); + sb.AppendLine(Analyze(exp.NewExpression).InsertIndent()); + } + + return sb.ToString().RemoveLastReturn(); + } + + internal static string Analyze(MemberInfo info) + { + var sb = new StringBuilder(); + + sb.AppendLine($"** MemberInfo"); + sb.AppendLine($"Name\r\n {info.Name}"); + sb.AppendLine($"MemberType\r\n {info.MemberType}"); + + var s = sb.ToString().RemoveLastReturn(); + return sb.ToString().RemoveLastReturn(); + } + + internal static string Analyze(MethodInfo info) + { + var sb = new StringBuilder(); + + sb.AppendLine($"** MethodInfo"); + sb.AppendLine($"Name\r\n {info.Name}"); + sb.AppendLine($"ReturnType\r\n {info.ReturnType.Name}"); + + var prms = info.GetParameters().ToList(); + + sb.AppendLine($"Parameters count\r\n {prms.Count()}"); + foreach (var p in prms) + { + sb.AppendLine($"- index : {prms.IndexOf(p)}"); + sb.AppendLine(Analyze(p).InsertIndent()); + } + + return sb.ToString().RemoveLastReturn(); + } + + internal static string Analyze(ParameterInfo info) + { + var sb = new StringBuilder(); + + sb.AppendLine($"** ParameterInfo"); + sb.AppendLine($"Name\r\n {info.Name}"); + sb.AppendLine($"ParameterType\r\n {info.ParameterType.Name}"); + + return sb.ToString().RemoveLastReturn(); + } + + internal static string RemoveLastReturn(this string s) + { + return Regex.Replace(s, @"\r\n$", ""); + } +} diff --git a/src/Carbunql.TypeSafe/Sql.cs b/src/Carbunql.TypeSafe/Sql.cs new file mode 100644 index 00000000..e1138910 --- /dev/null +++ b/src/Carbunql.TypeSafe/Sql.cs @@ -0,0 +1,282 @@ +using Carbunql.Annotations; +using Carbunql.Clauses; +using Carbunql.Building; +using System.Linq.Expressions; +using System.Runtime.CompilerServices; +using Carbunql.Values; +using System.Data.Common; +using System.Reflection; + +namespace Carbunql.TypeSafe; + +public interface ITableRowDefinition +{ + [IgnoreMapping] + TableDefinitionClause TableDefinition { get; set; } +} + +public class FluentSelectQuery : SelectQuery +{ + public FluentSelectQuery Select(Expression> expression) where T : class + { + var analyzed = ExpressionReader.Analyze(expression); + + var body = (NewExpression)expression.Body; + + /* +* LambdaExpression +NodeType + Lambda +Type + Func`1 +Name + "" +ReturnType + <>f__AnonymousType0`1[System.Int32] +Body + * NewExpression + NodeType + New + Type + <>f__AnonymousType0`1 + Arguments.Count + 1 + - index : 0 + * MemberExpression + NodeType + MemberAccess + Type + Int32 + Member + ** MemberInfo + Name + sale_id + MemberType + Property + Expression + * MemberExpression + NodeType + MemberAccess + Type + sale + Member + ** MemberInfo + Name + a + MemberType + Field + Expression + * ConstantExpression + NodeType + Constant + Type + <>c__DisplayClass4_0 + Value + Carbunql.TypeSafe.Test.SingleTableTest+<>c__DisplayClass4_0 + Members.Count + 1 + - index : 0 + ** MemberInfo + Name + id + MemberType + Property +Parameters count + 0 + */ + + var parameterCount = this.GetParameters().Count(); + Func addParameter = (obj) => + { + var pname = $"{DbmsConfiguration.PlaceholderIdentifier}p{parameterCount}"; + parameterCount++; + AddParameter(pname, obj); + return pname; + }; + + if (body.Members != null) + { + var cnt = body.Members.Count(); + for (var i = 0; i < cnt; i++) + { + var alias = body.Members[i].Name; + + if (TryToValue(body.Arguments[i], addParameter, out var value)) + { + this.Select(value).As(alias); + } + + } + } + + return this; + } + + public bool TryToValue(Expression exp, Func addParameter, out string value) + { + value = string.Empty; + + //var type = exp.Type; + + Func fn = (obj, tp) => + { + if (obj == null) + { + return "null"; + } + else if (tp == typeof(string)) + { + if (string.IsNullOrEmpty(obj.ToString())) + { + return "''"; + } + else + { + return addParameter(obj); + } + } + else if (tp == typeof(DateTime)) + { + return addParameter(obj); + } + else + { + var dbtype = DbmsConfiguration.ToDbType(tp); + return $"cast({obj} as {dbtype})"; + } + }; + + if (exp is MemberExpression mem) + { + if (mem.Expression is MemberExpression && typeof(ITableRowDefinition).IsAssignableFrom(mem.Expression.Type)) + { + //column + var table = ((MemberExpression)mem.Expression).Member.Name; + var column = mem.Member.Name; + value = $"{table}.{column}"; + return true; + } + else if (mem.Expression is ConstantExpression ce) + { + //variable + var val = mem.CompileAndInvoke(); + value = fn(val, mem.Type); + return true; + } + } + else if (exp is ConstantExpression ce) + { + // static value + value = fn(ce.Value, ce.Type); + return true; + } + else if (exp is NewExpression ne) + { + // ex. new Datetime + value = fn(ne.CompileAndInvoke(), ne.Type); + return true; + } + else if (exp is BinaryExpression be) + { + if (be.NodeType == ExpressionType.Coalesce) + { + + if (TryToValue(be.Left, addParameter, out var left) && TryToValue(be.Right, addParameter, out var right)) + { + value = $"{DbmsConfiguration.CoalesceFunctionName}({left}, {right})"; + return true; + } + } + + } + return false; + } + + private void Select(Type type, object? value, string alias, Func getParameterName) + { + if (value == null) + { + this.Select(new LiteralValue()).As(alias); + } + else if (type == typeof(string) || type == typeof(DateTime)) + { + var pname = getParameterName(); + var val = AddParameter(DbmsConfiguration.PlaceholderIdentifier + pname, value); + + this.Select(val).As(alias); + } + else + { + var val = new LiteralValue(value.ToString()); + var dbtype = new LiteralValue(DbmsConfiguration.ToDbType(type)); + var arg = new AsArgument(val, dbtype); + var functionValue = new FunctionValue("cast", arg); + + this.Select(functionValue).As(alias); + } + } + +} + +internal static class ExpressionExtension +{ + //internal static object? CompileAndInvoke(this MemberExpression exp) + //{ + // var method = typeof(ExpressionExtension) + // .GetMethod(nameof(CompileAndInvokeCore), BindingFlags.NonPublic | BindingFlags.Static)! + // .MakeGenericMethod(exp.Type); + + // return method.Invoke(null, new object[] { exp }); + //} + + //internal static T CompileAndInvokeCore(this MemberExpression exp) + //{ + // var lm = Expression.Lambda>(exp); + // return lm.Compile().Invoke(); + //} + + internal static object? CompileAndInvoke(this NewExpression exp) + { + var delegateType = typeof(Func<>).MakeGenericType(exp.Type); + var lambda = Expression.Lambda(delegateType, exp); + var compiledLambda = lambda.Compile(); + return compiledLambda.DynamicInvoke(); + } + + internal static object? CompileAndInvoke(this MemberExpression exp) + { + var delegateType = typeof(Func<>).MakeGenericType(exp.Type); + var lambda = Expression.Lambda(delegateType, exp); + var compiledLambda = lambda.Compile(); + return compiledLambda.DynamicInvoke(); + } +} + +/// +/// Only function definitions are written for use in expression trees. +/// The actual situation is in ExpressionExtension.cs. +/// () where T : ITableRowDefinition, new() + { + var instance = new T(); + instance.TableDefinition = TableDefinitionClauseFactory.Create(); + return instance; + } + + public static FluentSelectQuery From(Expression> expression) where T : ITableRowDefinition + { + var sq = new FluentSelectQuery(); + + var alias = ((MemberExpression)expression.Body).Member.Name; + + //execute + var compiledExpression = expression.Compile(); + var result = compiledExpression(); + + sq.From(result.TableDefinition).As(alias); + + return sq; + } +} \ No newline at end of file diff --git a/src/Carbunql.sln b/src/Carbunql.sln index 8fb83bc5..e19f36cc 100644 --- a/src/Carbunql.sln +++ b/src/Carbunql.sln @@ -27,7 +27,11 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Demo.Migration", "..\demo\M EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Demo.Parse", "..\demo\Parse\Demo.Parse.csproj", "{42FA31DD-B42A-4F88-BEDE-6BE9BF31BE2D}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Carbunql.Annotation.Test", "..\test\Carbunql.Annotation.Test\Carbunql.Annotation.Test.csproj", "{E76AE876-D280-401B-BE02-7500B608E320}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Carbunql.Annotation.Test", "..\test\Carbunql.Annotation.Test\Carbunql.Annotation.Test.csproj", "{E76AE876-D280-401B-BE02-7500B608E320}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Carbunql.TypeSafe", "Carbunql.TypeSafe\Carbunql.TypeSafe.csproj", "{8002213B-66D6-407E-BCD4-3E4092C24652}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Carbunql.TypeSafe.Test", "..\test\Carbunql.TypeSafe.Test\Carbunql.TypeSafe.Test.csproj", "{EA3034E3-564E-46B4-9FA8-B0A10D9DCBCE}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -87,6 +91,14 @@ Global {E76AE876-D280-401B-BE02-7500B608E320}.Debug|Any CPU.Build.0 = Debug|Any CPU {E76AE876-D280-401B-BE02-7500B608E320}.Release|Any CPU.ActiveCfg = Release|Any CPU {E76AE876-D280-401B-BE02-7500B608E320}.Release|Any CPU.Build.0 = Release|Any CPU + {8002213B-66D6-407E-BCD4-3E4092C24652}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8002213B-66D6-407E-BCD4-3E4092C24652}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8002213B-66D6-407E-BCD4-3E4092C24652}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8002213B-66D6-407E-BCD4-3E4092C24652}.Release|Any CPU.Build.0 = Release|Any CPU + {EA3034E3-564E-46B4-9FA8-B0A10D9DCBCE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EA3034E3-564E-46B4-9FA8-B0A10D9DCBCE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EA3034E3-564E-46B4-9FA8-B0A10D9DCBCE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EA3034E3-564E-46B4-9FA8-B0A10D9DCBCE}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/src/Carbunql/Building/SelectQueryExtension.cs b/src/Carbunql/Building/SelectQueryExtension.cs index c8763a29..a0277a6a 100644 --- a/src/Carbunql/Building/SelectQueryExtension.cs +++ b/src/Carbunql/Building/SelectQueryExtension.cs @@ -1,4 +1,5 @@ using Carbunql.Clauses; +using Carbunql.Definitions; using Carbunql.Tables; namespace Carbunql.Building; @@ -76,6 +77,12 @@ public static FromClause From(this SelectQuery source, string schema, string tab return source.From(selectableTable); } + public static FromClause From(this SelectQuery source, ITable table) + { + var selectableTable = new PhysicalTable(table.Schema, table.Table).ToSelectable(); + return source.From(selectableTable); + } + /// /// Specifies the source table for the SELECT query. /// diff --git a/src/Carbunql/Clauses/TableDefinitionClause.cs b/src/Carbunql/Clauses/TableDefinitionClause.cs index abcd33b0..bdbe5350 100644 --- a/src/Carbunql/Clauses/TableDefinitionClause.cs +++ b/src/Carbunql/Clauses/TableDefinitionClause.cs @@ -39,6 +39,10 @@ public TableDefinitionClause(string schema, string table) Table = table; } + public IEnumerable ColumnNames => this.Where(x => !string.IsNullOrEmpty(x.ColumnName)).Select(x => x.ColumnName); + + public IEnumerable PrimaryKeys => this.OfType().SelectMany(x => x.PrimaryKeyMaps.Select(y => y.ColumnName)); + /// /// Gets the tokens of the collection. /// diff --git a/src/Carbunql/DbmsConfiguration.cs b/src/Carbunql/DbmsConfiguration.cs index 72f92c7c..1b34b937 100644 --- a/src/Carbunql/DbmsConfiguration.cs +++ b/src/Carbunql/DbmsConfiguration.cs @@ -202,7 +202,7 @@ public static string ToDbType(Type propertyType) } else { - throw new ArgumentException("Unsupported property type"); + throw new ArgumentException($"Unsupported property type :{propertyType}"); } } diff --git a/test/Carbunql.TypeSafe.Test/Carbunql.TypeSafe.Test.csproj b/test/Carbunql.TypeSafe.Test/Carbunql.TypeSafe.Test.csproj new file mode 100644 index 00000000..4c6fc120 --- /dev/null +++ b/test/Carbunql.TypeSafe.Test/Carbunql.TypeSafe.Test.csproj @@ -0,0 +1,28 @@ + + + + net6.0 + 12.0 + enable + enable + + false + true + + + + + + + + + + + + + + + + + + diff --git a/test/Carbunql.TypeSafe.Test/SingleTableTest.cs b/test/Carbunql.TypeSafe.Test/SingleTableTest.cs new file mode 100644 index 00000000..b80be565 --- /dev/null +++ b/test/Carbunql.TypeSafe.Test/SingleTableTest.cs @@ -0,0 +1,184 @@ +using Carbunql.Clauses; +using Xunit.Abstractions; + +namespace Carbunql.TypeSafe.Test; + +public class SingleTableTest +{ + public SingleTableTest(ITestOutputHelper output) + { + Output = output; + } + + private ITestOutputHelper Output { get; } + + [Fact] + public void SelectTest() + { + var a = Sql.DefineTable(); + + var query = Sql.From(() => a) + .Select(() => new + { + a.sale_id, + }); + + var actual = query.ToText(); + Output.WriteLine(query.ToText()); + + var expect = @"SELECT + a.sale_id +FROM + sale AS a"; + + Assert.Equal(expect, actual, true, true, true); + } + + [Fact] + public void AliasTest() + { + var a = Sql.DefineTable(); + + var query = Sql.From(() => a) + .Select(() => new + { + id = a.sale_id, + a.unit_price, + a.product_name + }); + + var actual = query.ToText(); + Output.WriteLine(actual); + + var expect = @"SELECT + a.sale_id AS id, + a.unit_price, + a.product_name +FROM + sale AS a"; + + Assert.Equal(expect, actual, true, true, true); + } + + [Fact] + public void LiteralTest() + { + var a = Sql.DefineTable(); + + var query = Sql.From(() => a) + .Select(() => new + { + id = 1, + value = (long)10, + rate = (decimal)0.1, + tf_value = true, + remarks = "test", + created_at = new DateTime(2000, 1, 1, 10, 10, 0) + }); + + var actual = query.ToText(); + Output.WriteLine(actual); + + var expect = @"/* + :p0 = 'test' + :p1 = 2000/01/01 10:10:00 +*/ +SELECT + CAST(1 AS integer) AS id, + CAST(10 AS bigint) AS value, + CAST(0.1 AS numeric) AS rate, + CAST(True AS boolean) AS tf_value, + :p0 AS remarks, + :p1 AS created_at +FROM + sale AS a"; + + Assert.Equal(expect, actual, true, true, true); + } + + [Fact] + public void VariableTest() + { + var a = Sql.DefineTable(); + + var id = 1; + var value = (long)10; + var rate = (decimal)0.1; + var remarks = "test"; + var tf_value = true; + var created_at = new DateTime(2000, 1, 1, 10, 10, 0); + + var query = Sql.From(() => a) + .Select(() => new + { + id, + value, + rate, + tf_value, + remarks, + created_at + }); + + var actual = query.ToText(); + Output.WriteLine(actual); + + var expect = @"/* + :p0 = 'test' + :p1 = 2000/01/01 10:10:00 +*/ +SELECT + CAST(1 AS integer) AS id, + CAST(10 AS bigint) AS value, + CAST(0.1 AS numeric) AS rate, + CAST(True AS boolean) AS tf_value, + :p0 AS remarks, + :p1 AS created_at +FROM + sale AS a"; + + Assert.Equal(expect, actual, true, true, true); + } + + // ZASQLCӊ֐RAWiɗ萔gƂAcurrent_timestmapjA + // ϊ(coalesce, case)A + // (like, exists)A + + [Fact] + public void CSharpFunction_Coalesce() + { + var a = Sql.DefineTable(); + + var query = Sql.From(() => a) + .Select(() => new + { + value = a.sale_id ?? 0, + text = a.product_name ?? "" + }); + + var actual = query.ToText(); + Output.WriteLine(actual); + + var expect = @"SELECT + COALESCE(a.sale_id, CAST(0 AS integer)) AS value, + COALESCE(a.product_name, '') AS text +FROM + sale AS a"; + + Assert.Equal(expect, actual, true, true, true); + } + + public record sale( + int? sale_id, + string product_name, + int quantity, + decimal unit_price + ) : ITableRowDefinition + { + // no arguments constructor. + // Since it is used as a definition, it has no particular meaning as a value. + public sale() : this(0, "", 0, 0) { } + + // interface property + TableDefinitionClause ITableRowDefinition.TableDefinition { get; set; } = null!; + } +} \ No newline at end of file From d4aaa0f86a97373944d4e2a1edb948cb87957779 Mon Sep 17 00:00:00 2001 From: mk3008 Date: Tue, 14 May 2024 23:14:53 +0900 Subject: [PATCH 02/23] Bug Fix: Fixed incorrect syntax when over clause had zero arguments. --- src/Carbunql/Clauses/WindowDefinition.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Carbunql/Clauses/WindowDefinition.cs b/src/Carbunql/Clauses/WindowDefinition.cs index 7a8181d5..e7fc4b29 100644 --- a/src/Carbunql/Clauses/WindowDefinition.cs +++ b/src/Carbunql/Clauses/WindowDefinition.cs @@ -181,8 +181,6 @@ public IEnumerable GetTokens(Token? parent) yield break; } - if (PartitionBy == null && OrderBy == null) yield break; - var bracket = Token.ReservedBracketStart(this, parent); yield return bracket; From 34eb80a796e37eefbef5624d68ed49ce4447b245 Mon Sep 17 00:00:00 2001 From: mk3008 Date: Tue, 14 May 2024 23:18:00 +0900 Subject: [PATCH 03/23] Added type-safe SQL functionality. Operator SQL compatible Supports SQL for Math class methods Added function to convert without converting to SQL (Sql.Raw function) Adding reservation functions (Now, CurrentTimestamp, RowNumber) --- src/Carbunql.TypeSafe/Sql.cs | 211 +++++++++++++++--- src/Carbunql/DbmsConfiguration.cs | 77 +++++++ .../Carbunql.TypeSafe.Test/SingleTableTest.cs | 138 +++++++++++- 3 files changed, 395 insertions(+), 31 deletions(-) diff --git a/src/Carbunql.TypeSafe/Sql.cs b/src/Carbunql.TypeSafe/Sql.cs index e1138910..18b34a03 100644 --- a/src/Carbunql.TypeSafe/Sql.cs +++ b/src/Carbunql.TypeSafe/Sql.cs @@ -1,11 +1,9 @@ using Carbunql.Annotations; -using Carbunql.Clauses; using Carbunql.Building; -using System.Linq.Expressions; -using System.Runtime.CompilerServices; -using Carbunql.Values; +using Carbunql.Clauses; +using System.Data; using System.Data.Common; -using System.Reflection; +using System.Linq.Expressions; namespace Carbunql.TypeSafe; @@ -112,6 +110,16 @@ Parameters count return this; } + + public string ToValue(Expression exp, Func addParameter) + { + if (TryToValue(exp, addParameter, out var value)) + { + return value; + } + throw new Exception(); + } + public bool TryToValue(Expression exp, Func addParameter, out string value) { value = string.Empty; @@ -148,7 +156,23 @@ public bool TryToValue(Expression exp, Func addParameter, out s if (exp is MemberExpression mem) { - if (mem.Expression is MemberExpression && typeof(ITableRowDefinition).IsAssignableFrom(mem.Expression.Type)) + var tp = mem.Member.DeclaringType; + + if (tp == typeof(Sql)) + { + if (mem.Member.Name == nameof(Sql.Now)) + { + value = DbmsConfiguration.GetNowCommandLogic(); + return true; + } + if (mem.Member.Name == nameof(Sql.CurrentTimestamp)) + { + value = DbmsConfiguration.GetCurrentTimestampCommandLogic(); + return true; + } + throw new InvalidProgramException(); + } + if (mem.Expression is MemberExpression && typeof(ITableRowDefinition).IsAssignableFrom(tp)) { //column var table = ((MemberExpression)mem.Expression).Member.Name; @@ -178,44 +202,156 @@ public bool TryToValue(Expression exp, Func addParameter, out s } else if (exp is BinaryExpression be) { - if (be.NodeType == ExpressionType.Coalesce) + if (TryToValue(be.Left, addParameter, out var left) && TryToValue(be.Right, addParameter, out var right)) { - - if (TryToValue(be.Left, addParameter, out var left) && TryToValue(be.Right, addParameter, out var right)) + if (be.NodeType == ExpressionType.Coalesce) { value = $"{DbmsConfiguration.CoalesceFunctionName}({left}, {right})"; return true; } + if (be.NodeType == ExpressionType.Add) + { + value = $"{left} + {right}"; + return true; + } + if (be.NodeType == ExpressionType.Subtract) + { + value = $"{left} - {right}"; + return true; + } + if (be.NodeType == ExpressionType.Multiply) + { + value = $"{left} * {right}"; + return true; + } + if (be.NodeType == ExpressionType.Divide) + { + value = $"{left} / {right}"; + return true; + } + if (be.NodeType == ExpressionType.Modulo) + { + value = DbmsConfiguration.GetModuloCommandLogic(left, right); + return true; + } } - } - return false; - } - - private void Select(Type type, object? value, string alias, Func getParameterName) - { - if (value == null) + else if (exp is UnaryExpression ue) { - this.Select(new LiteralValue()).As(alias); + if (TryToValue(ue.Operand, addParameter, out var val)) + { + if (ue.NodeType == ExpressionType.Convert) + { + var dbtype = DbmsConfiguration.ToDbType(ue.Type); + value = $"cast({val} as {dbtype})"; + return true; + } + } } - else if (type == typeof(string) || type == typeof(DateTime)) + else if (exp is MethodCallExpression mce) { - var pname = getParameterName(); - var val = AddParameter(DbmsConfiguration.PlaceholderIdentifier + pname, value); + var args = mce.Arguments.Select(x => ToValue(x, addParameter)); - this.Select(val).As(alias); - } - else - { - var val = new LiteralValue(value.ToString()); - var dbtype = new LiteralValue(DbmsConfiguration.ToDbType(type)); - var arg = new AsArgument(val, dbtype); - var functionValue = new FunctionValue("cast", arg); + var tp = mce.Method.DeclaringType?.FullName; - this.Select(functionValue).As(alias); + if (tp == typeof(Math).ToString()) + { + if (mce.Method.Name == nameof(Math.Truncate)) + { + value = DbmsConfiguration.GetTruncateCommandLogic(args); + return true; + } + if (mce.Method.Name == nameof(Math.Floor)) + { + value = DbmsConfiguration.GetFloorCommandLogic(args); + return true; + } + if (mce.Method.Name == nameof(Math.Ceiling)) + { + value = DbmsConfiguration.GetCeilingCommandLogic(args); + return true; + } + if (mce.Method.Name == nameof(Math.Round)) + { + value = DbmsConfiguration.GetRoundCommandLogic(args); + return true; + } + } + if (tp == typeof(Sql).ToString()) + { + //reserved command + if (mce.Method.Name == nameof(Sql.Raw)) + { + var arg = (ConstantExpression)mce.Arguments.First(); + value = arg.Value!.ToString()!; + return true; + } + if (mce.Method.Name == nameof(Sql.RowNumber)) + { + if (mce.Arguments.Count == 0) + { + value = DbmsConfiguration.GetRowNumberCommandLogic(); + return true; + } + if (mce.Arguments.Count == 2) + { + var argList = mce.Arguments.ToList(); + var arg1st = (NewExpression)argList[0]; + var arg2nd = (NewExpression)argList[1]; + var arg1stText = string.Join(",", arg1st.Arguments.Select(x => ToValue(x, addParameter))); + var arg2ndText = string.Join(",", arg2nd.Arguments.Select(x => ToValue(x, addParameter))); + + value = DbmsConfiguration.GetRowNumberPartitionByOrderByCommandLogic(arg1stText, arg2ndText); + return true; + } + } + if (mce.Method.Name == nameof(Sql.RowNumberOrderbyBy)) + { + var arg1st = (NewExpression)mce.Arguments.First(); + + var arg1stText = string.Join(",", arg1st.Arguments.Select(x => ToValue(x, addParameter))); + + value = DbmsConfiguration.GetRowNumberOrderByCommandLogic(arg1stText); + return true; + } + if (mce.Method.Name == nameof(Sql.RowNumberPartitionBy)) + { + var arg1st = (NewExpression)mce.Arguments.First(); + + var arg1stText = string.Join(",", arg1st.Arguments.Select(x => ToValue(x, addParameter))); + + value = DbmsConfiguration.GetRowNumberPartitionByCommandLogic(arg1stText); + return true; + } + throw new InvalidProgramException(); + } } + return false; } + //private void Select(Type type, object? value, string alias, Func getParameterName) + //{ + // if (value == null) + // { + // this.Select(new LiteralValue()).As(alias); + // } + // else if (type == typeof(string) || type == typeof(DateTime)) + // { + // var pname = getParameterName(); + // var val = AddParameter(DbmsConfiguration.PlaceholderIdentifier + pname, value); + + // this.Select(val).As(alias); + // } + // else + // { + // var val = new LiteralValue(value.ToString()); + // var dbtype = new LiteralValue(DbmsConfiguration.ToDbType(type)); + // var arg = new AsArgument(val, dbtype); + // var functionValue = new FunctionValue("cast", arg); + + // this.Select(functionValue).As(alias); + // } + //} } internal static class ExpressionExtension @@ -279,4 +415,21 @@ public static FluentSelectQuery From(Expression> expression) where T return sq; } + + public static string Raw(string command) + { + return command; + } + + public static string CurrentTimestamp => string.Empty; + + public static string Now => string.Empty; + + public static string RowNumber() => string.Empty; + + public static string RowNumber(object partition, object order) => string.Empty; + + public static string RowNumberPartitionBy(object partition) => string.Empty; + + public static string RowNumberOrderbyBy(object order) => string.Empty; } \ No newline at end of file diff --git a/src/Carbunql/DbmsConfiguration.cs b/src/Carbunql/DbmsConfiguration.cs index 1b34b937..0f62259d 100644 --- a/src/Carbunql/DbmsConfiguration.cs +++ b/src/Carbunql/DbmsConfiguration.cs @@ -302,4 +302,81 @@ private static string GetDefaultIndexName(string propertyName) } public static Func GetDefaultIndexNameLogic { get; set; } = GetDefaultIndexName; + + private static string GetModuloCommand(string dividend, string divisor) + { + return dividend + " % " + divisor; + } + + public static Func GetModuloCommandLogic { get; set; } = GetModuloCommand; + + private static string GetTruncateCommand(IEnumerable values) + { + return $"trunc({string.Join(",", values)})"; + } + + public static Func, string> GetTruncateCommandLogic { get; set; } = GetTruncateCommand; + + private static string GetCeilingCommand(IEnumerable values) + { + return $"ceil({string.Join(",", values)})"; + } + + public static Func, string> GetCeilingCommandLogic { get; set; } = GetCeilingCommand; + + private static string GetFloorCommand(IEnumerable values) + { + return $"floor({string.Join(",", values)})"; + } + + public static Func, string> GetFloorCommandLogic { get; set; } = GetFloorCommand; + + private static string GetRoundCommand(IEnumerable values) + { + return $"round({string.Join(",", values)})"; + } + + public static Func, string> GetRoundCommandLogic { get; set; } = GetRoundCommand; + + private static string GetNowCommand() + { + return $"now()"; + } + + public static Func GetNowCommandLogic { get; set; } = GetNowCommand; + + private static string GetCurrentTimestampCommand() + { + return $"current_timestamp"; + } + + public static Func GetCurrentTimestampCommandLogic { get; set; } = GetCurrentTimestampCommand; + + private static string GetRowNumberCommand() + { + return $"row_number() over()"; + } + + public static Func GetRowNumberCommandLogic { get; set; } = GetRowNumberCommand; + + private static string GetRowNumberPartitionByOrderByCommand(string partition, string order) + { + return $"row_number() over(partition by {partition} order by {order})"; + } + + public static Func GetRowNumberPartitionByOrderByCommandLogic { get; set; } = GetRowNumberPartitionByOrderByCommand; + + private static string GetRowNumberOrderByCommand(string order) + { + return $"row_number() over(order by {order})"; + } + + public static Func GetRowNumberOrderByCommandLogic { get; set; } = GetRowNumberOrderByCommand; + + private static string GetRowNumberPartitionByCommand(string partition) + { + return $"row_number() over(partition by {partition})"; + } + + public static Func GetRowNumberPartitionByCommandLogic { get; set; } = GetRowNumberPartitionByCommand; } \ No newline at end of file diff --git a/test/Carbunql.TypeSafe.Test/SingleTableTest.cs b/test/Carbunql.TypeSafe.Test/SingleTableTest.cs index b80be565..4804b9f5 100644 --- a/test/Carbunql.TypeSafe.Test/SingleTableTest.cs +++ b/test/Carbunql.TypeSafe.Test/SingleTableTest.cs @@ -1,4 +1,5 @@ using Carbunql.Clauses; +using System.Diagnostics; using Xunit.Abstractions; namespace Carbunql.TypeSafe.Test; @@ -139,8 +140,8 @@ public void VariableTest() Assert.Equal(expect, actual, true, true, true); } - // ZASQLCӊ֐RAWiɗ萔gƂAcurrent_timestmapjA - // ϊ(coalesce, case)A + // SQLCӊ֐RAWiɗ萔gƂAcurrent_timestmapjA + // ϊ(case)A // (like, exists)A [Fact] @@ -167,6 +168,139 @@ public void CSharpFunction_Coalesce() Assert.Equal(expect, actual, true, true, true); } + [Fact] + public void CSharpFunction_Operator() + { + var a = Sql.DefineTable(); + + var query = Sql.From(() => a) + .Select(() => new + { + v_add = a.unit_price + a.sale_id, + v_subtract = a.unit_price - a.sale_id, + v_multiply = a.unit_price * a.sale_id, + v_divide = a.unit_price / a.sale_id, + v_modulo = a.unit_price % a.sale_id, + tax = a.unit_price * a.quantity * (decimal)0.1 + }); + + var actual = query.ToText(); + Output.WriteLine(actual); + + var expect = @"SELECT + CAST(a.unit_price AS numeric) + CAST(a.sale_id AS numeric) AS v_add, + CAST(a.unit_price AS numeric) - CAST(a.sale_id AS numeric) AS v_subtract, + CAST(a.unit_price AS numeric) * CAST(a.sale_id AS numeric) AS v_multiply, + CAST(a.unit_price AS numeric) / CAST(a.sale_id AS numeric) AS v_divide, + CAST(a.unit_price AS numeric) % CAST(a.sale_id AS numeric) AS v_modulo, + a.unit_price * CAST(a.quantity AS numeric) * CAST(0.1 AS numeric) AS tax +FROM + sale AS a"; + + Assert.Equal(expect, actual, true, true, true); + } + + [Fact] + public void CSharpFunction_Math() + { + var a = Sql.DefineTable(); + + var query = Sql.From(() => a) + .Select(() => new + { + v_truncate = Math.Truncate(a.unit_price), + v_floor = Math.Floor(a.unit_price), + v_ceiling = Math.Ceiling(a.unit_price), + v_round_arg1 = Math.Round(a.unit_price), + v_round_arg2 = Math.Round(a.unit_price, 2), + test = Math.Truncate(a.unit_price * a.quantity), + }); + + var actual = query.ToText(); + Output.WriteLine(actual); + + var expect = @"SELECT + TRUNC(a.unit_price) AS v_truncate, + FLOOR(a.unit_price) AS v_floor, + CEIL(a.unit_price) AS v_ceiling, + ROUND(a.unit_price) AS v_round_arg1, + ROUND(a.unit_price, CAST(2 AS integer)) AS v_round_arg2, + TRUNC(a.unit_price * CAST(a.quantity AS numeric)) AS test +FROM + sale AS a"; + + Assert.Equal(expect, actual, true, true, true); + } + + [Fact] + public void RawCommand() + { + var a = Sql.DefineTable(); + + var query = Sql.From(() => a) + .Select(() => new + { + rawcommand = Sql.Raw("current_timestamp") + }); + + var actual = query.ToText(); + Output.WriteLine(actual); + + var expect = @"SELECT + current_timestamp AS rawcommand +FROM + sale AS a"; + + Assert.Equal(expect, actual, true, true, true); + } + + [Fact] + public void ReservedCommand() + { + var a = Sql.DefineTable(); + + var query = Sql.From(() => a) + .Select(() => new + { + now_command = Sql.Now, + timestamp_commend = Sql.CurrentTimestamp, + row_num = Sql.RowNumber(), + row_num_partiton_order = Sql.RowNumber(new { a.product_name, a.unit_price }, new { a.quantity, a.sale_id }), + row_num_partition = Sql.RowNumberPartitionBy(new { a.product_name, a.unit_price }), + row_num_order = Sql.RowNumberOrderbyBy(new { a.product_name, a.unit_price }) + }); + + var actual = query.ToText(); + Output.WriteLine(actual); + + var expect = @"SELECT + NOW() AS now_command, + current_timestamp AS timestamp_commend, + ROW_NUMBER() OVER() AS row_num, + ROW_NUMBER() OVER( + PARTITION BY + a.product_name, + a.unit_price + ORDER BY + a.quantity, + a.sale_id + ) AS row_num_partiton_order, + ROW_NUMBER() OVER( + PARTITION BY + a.product_name, + a.unit_price + ) AS row_num_partition, + ROW_NUMBER() OVER( + ORDER BY + a.product_name, + a.unit_price + ) AS row_num_order +FROM + sale AS a"; + + Assert.Equal(expect, actual, true, true, true); + } + public record sale( int? sale_id, string product_name, From 380c8fde87380be291f90de56fd0b33586962aa3 Mon Sep 17 00:00:00 2001 From: mk3008 Date: Wed, 15 May 2024 22:22:39 +0900 Subject: [PATCH 04/23] Enhanced type-safe query builder functionality Supports trinomial expressions (case statements). Eliminate excessive type conversion. --- src/Carbunql.TypeSafe/ExpressionReader.cs | 23 ++ src/Carbunql.TypeSafe/Sql.cs | 79 ++++++- .../Carbunql.TypeSafe.Test/SingleTableTest.cs | 220 +++++++++++++++++- 3 files changed, 308 insertions(+), 14 deletions(-) diff --git a/src/Carbunql.TypeSafe/ExpressionReader.cs b/src/Carbunql.TypeSafe/ExpressionReader.cs index e4b511c4..f0c133e4 100644 --- a/src/Carbunql.TypeSafe/ExpressionReader.cs +++ b/src/Carbunql.TypeSafe/ExpressionReader.cs @@ -89,6 +89,10 @@ public static string Analyze(Expression exp) { return Analyze(be); } + else if (exp is ConditionalExpression ce) + { + return Analyze(ce); + } return $"not support : {exp.NodeType}"; } @@ -356,6 +360,25 @@ internal static string Analyze(ParameterInfo info) return sb.ToString().RemoveLastReturn(); } + internal static string Analyze(ConditionalExpression exp) + { + var sb = new StringBuilder(); + sb.AppendLine($"* MethodCallExpression"); + sb.AppendLine($"NodeType\r\n {exp.NodeType}"); + sb.AppendLine($"Type\r\n {exp.Type.Name}"); + + sb.AppendLine("Test"); + sb.AppendLine(Analyze(exp.Test).InsertIndent()); + + sb.AppendLine("IfTrue"); + sb.AppendLine(Analyze(exp.IfTrue).InsertIndent()); + + sb.AppendLine("IfFalse"); + sb.AppendLine(Analyze(exp.IfFalse).InsertIndent()); + + return sb.ToString().RemoveLastReturn(); + } + internal static string RemoveLastReturn(this string s) { return Regex.Replace(s, @"\r\n$", ""); diff --git a/src/Carbunql.TypeSafe/Sql.cs b/src/Carbunql.TypeSafe/Sql.cs index 18b34a03..a43508c3 100644 --- a/src/Carbunql.TypeSafe/Sql.cs +++ b/src/Carbunql.TypeSafe/Sql.cs @@ -1,6 +1,8 @@ -using Carbunql.Annotations; +using Carbunql.Analysis.Parser; +using Carbunql.Annotations; using Carbunql.Building; using Carbunql.Clauses; +using Carbunql.Values; using System.Data; using System.Data.Common; using System.Linq.Expressions; @@ -149,8 +151,9 @@ public bool TryToValue(Expression exp, Func addParameter, out s } else { - var dbtype = DbmsConfiguration.ToDbType(tp); - return $"cast({obj} as {dbtype})"; + //var dbtype = DbmsConfiguration.ToDbType(tp); + //return $"cast({obj} as {dbtype})"; + return obj!.ToString()!; } }; @@ -234,6 +237,36 @@ public bool TryToValue(Expression exp, Func addParameter, out s value = DbmsConfiguration.GetModuloCommandLogic(left, right); return true; } + if (be.NodeType == ExpressionType.Equal) + { + value = value = $"{left} = {right}"; + return true; + } + if (be.NodeType == ExpressionType.NotEqual) + { + value = value = $"{left} <> {right}"; + return true; + } + if (be.NodeType == ExpressionType.GreaterThan) + { + value = value = $"{left} > {right}"; + return true; + } + if (be.NodeType == ExpressionType.GreaterThanOrEqual) + { + value = value = $"{left} >= {right}"; + return true; + } + if (be.NodeType == ExpressionType.LessThan) + { + value = value = $"{left} < {right}"; + return true; + } + if (be.NodeType == ExpressionType.LessThanOrEqual) + { + value = value = $"{left} <= {right}"; + return true; + } } } else if (exp is UnaryExpression ue) @@ -243,6 +276,18 @@ public bool TryToValue(Expression exp, Func addParameter, out s if (ue.NodeType == ExpressionType.Convert) { var dbtype = DbmsConfiguration.ToDbType(ue.Type); + + ////excludes excessive casts + //if (val.StartsWith("cast(")) + //{ + // var fv = (FunctionValue)ValueParser.Parse(val); + // if (fv.Arguments.Count == 1 && fv.Arguments[0] is AsArgument arg && dbtype == arg.Type.ToText()) + // { + // value = val; + // return true; + // } + //} + value = $"cast({val} as {dbtype})"; return true; } @@ -326,6 +371,34 @@ public bool TryToValue(Expression exp, Func addParameter, out s throw new InvalidProgramException(); } } + else if (exp is ConditionalExpression cnd) + { + var test = ToValue(cnd.Test, addParameter); + var ifTrue = ToValue(cnd.IfTrue, addParameter); + var ifFalse = ToValue(cnd.IfFalse, addParameter); + + if (ifFalse.StartsWith("case ")) + { + var caseExpression = CaseExpressionParser.Parse(ifFalse); + if (caseExpression.CaseCondition is null) + { + var we = WhenExpressionParser.Parse($"when {test} then {ifTrue}"); + caseExpression.WhenExpressions.Insert(0, we); + value = caseExpression.ToText(); + return true; + } + else + { + value = $"case when {test} then {ifTrue} else {ifFalse} end"; + return true; + } + } + else + { + value = $"case when {test} then {ifTrue} else {ifFalse} end"; + return true; + } + } return false; } diff --git a/test/Carbunql.TypeSafe.Test/SingleTableTest.cs b/test/Carbunql.TypeSafe.Test/SingleTableTest.cs index 4804b9f5..8a3f0fe7 100644 --- a/test/Carbunql.TypeSafe.Test/SingleTableTest.cs +++ b/test/Carbunql.TypeSafe.Test/SingleTableTest.cs @@ -85,10 +85,10 @@ public void LiteralTest() :p1 = 2000/01/01 10:10:00 */ SELECT - CAST(1 AS integer) AS id, - CAST(10 AS bigint) AS value, - CAST(0.1 AS numeric) AS rate, - CAST(True AS boolean) AS tf_value, + 1 AS id, + 10 AS value, + 0.1 AS rate, + True AS tf_value, :p0 AS remarks, :p1 AS created_at FROM @@ -128,10 +128,10 @@ public void VariableTest() :p1 = 2000/01/01 10:10:00 */ SELECT - CAST(1 AS integer) AS id, - CAST(10 AS bigint) AS value, - CAST(0.1 AS numeric) AS rate, - CAST(True AS boolean) AS tf_value, + 1 AS id, + 10 AS value, + 0.1 AS rate, + True AS tf_value, :p0 AS remarks, :p1 AS created_at FROM @@ -160,7 +160,7 @@ public void CSharpFunction_Coalesce() Output.WriteLine(actual); var expect = @"SELECT - COALESCE(a.sale_id, CAST(0 AS integer)) AS value, + COALESCE(a.sale_id, 0) AS value, COALESCE(a.product_name, '') AS text FROM sale AS a"; @@ -193,7 +193,7 @@ public void CSharpFunction_Operator() CAST(a.unit_price AS numeric) * CAST(a.sale_id AS numeric) AS v_multiply, CAST(a.unit_price AS numeric) / CAST(a.sale_id AS numeric) AS v_divide, CAST(a.unit_price AS numeric) % CAST(a.sale_id AS numeric) AS v_modulo, - a.unit_price * CAST(a.quantity AS numeric) * CAST(0.1 AS numeric) AS tax + a.unit_price * CAST(a.quantity AS numeric) * 0.1 AS tax FROM sale AS a"; @@ -224,7 +224,7 @@ public void CSharpFunction_Math() FLOOR(a.unit_price) AS v_floor, CEIL(a.unit_price) AS v_ceiling, ROUND(a.unit_price) AS v_round_arg1, - ROUND(a.unit_price, CAST(2 AS integer)) AS v_round_arg2, + ROUND(a.unit_price, 2) AS v_round_arg2, TRUNC(a.unit_price * CAST(a.quantity AS numeric)) AS test FROM sale AS a"; @@ -301,6 +301,204 @@ ORDER BY Assert.Equal(expect, actual, true, true, true); } + [Fact] + public void Trinomial() + { + var a = Sql.DefineTable(); + + var query = Sql.From(() => a) + .Select(() => new + { + v_equal = a.sale_id == 1 ? 0 : 1, + v_not_equal = a.sale_id != 1 ? 0 : 1, + v_gt = a.sale_id < 1 ? 0 : 1, + v_ge = a.sale_id <= 1 ? 0 : 1, + v_lt = a.sale_id > 1 ? 0 : 1, + v_le = a.sale_id >= 1 ? 0 : 1, + }); + + var actual = query.ToText(); + Output.WriteLine(actual); + + var expect = @"SELECT + CASE + WHEN a.sale_id = CAST(1 AS integer) THEN 0 + ELSE 1 + END AS v_equal, + CASE + WHEN a.sale_id <> CAST(1 AS integer) THEN 0 + ELSE 1 + END AS v_not_equal, + CASE + WHEN a.sale_id < CAST(1 AS integer) THEN 0 + ELSE 1 + END AS v_gt, + CASE + WHEN a.sale_id <= CAST(1 AS integer) THEN 0 + ELSE 1 + END AS v_ge, + CASE + WHEN a.sale_id > CAST(1 AS integer) THEN 0 + ELSE 1 + END AS v_lt, + CASE + WHEN a.sale_id >= CAST(1 AS integer) THEN 0 + ELSE 1 + END AS v_le +FROM + sale AS a"; + + Assert.Equal(expect, actual, true, true, true); + } + + [Fact] + public void Trinomial_When() + { + var a = Sql.DefineTable(); + + var query = Sql.From(() => a) + .Select(() => new + { + v_nest = a.sale_id == 1 ? 10 : + a.sale_id == 2 ? 20 : + a.sale_id == 3 ? 30 : + 99, + }); + + var actual = query.ToText(); + Output.WriteLine(actual); + + var expect = @"SELECT + CASE + WHEN a.sale_id = CAST(1 AS integer) THEN 10 + WHEN a.sale_id = CAST(2 AS integer) THEN 20 + WHEN a.sale_id = CAST(3 AS integer) THEN 30 + ELSE 99 + END AS v_nest +FROM + sale AS a"; + + Assert.Equal(expect, actual, true, true, true); + } + + [Fact] + public void Trinomial_Nest() + { + var a = Sql.DefineTable(); + + var query = Sql.From(() => a) + .Select(() => new + { + v_nest = a.sale_id == 1 ? a.unit_price == 10 ? 11 : + a.unit_price == 20 ? 21 : + 91 : + a.sale_id == 2 ? a.unit_price == 10 ? 12 : + a.unit_price == 20 ? 22 : + 92 : + a.sale_id == 3 ? a.unit_price == 10 ? 31 : + a.unit_price == 10 ? 32 : + 93 : + 99, + }); + + var actual = query.ToText(); + Output.WriteLine(actual); + + var expect = @"SELECT + CASE + WHEN a.sale_id = CAST(1 AS integer) THEN CASE + WHEN a.unit_price = 10 THEN 11 + WHEN a.unit_price = 20 THEN 21 + ELSE 91 + END + WHEN a.sale_id = CAST(2 AS integer) THEN CASE + WHEN a.unit_price = 10 THEN 12 + WHEN a.unit_price = 20 THEN 22 + ELSE 92 + END + WHEN a.sale_id = CAST(3 AS integer) THEN CASE + WHEN a.unit_price = 10 THEN 31 + WHEN a.unit_price = 10 THEN 32 + ELSE 93 + END + ELSE 99 + END AS v_nest +FROM + sale AS a"; + + Assert.Equal(expect, actual, true, true, true); + } + + // //procrastinate + // //[Fact] + // public void Switch() + // { + // var a = Sql.DefineTable(); + + // var query = Sql.From(() => a) + // .Select(() => new + // { + // value = switch (a.sale_id) + // { + // default: + // 1; + // } + // }); + + // var actual = query.ToText(); + // Output.WriteLine(actual); + + // var expect = @"SELECT + // * + //FROM + // sale AS a"; + + // Assert.Equal(expect, actual, true, true, true); + // } + + /* //procrastinate + //[Fact] + public void SelectAll() + { + var a = Sql.DefineTable(); + + var query = Sql.From(() => a) + .Select(() => new { }); + + var actual = query.ToText(); + Output.WriteLine(actual); + + var expect = @"SELECT + * + FROM + sale AS a"; + + Assert.Equal(expect, actual, true, true, true); + } + + //procrastinate + //[Fact] + public void SelectTableAll() + { + var a = Sql.DefineTable(); + + var query = Sql.From(() => a) + .Select(() => new { a }); + + var actual = query.ToText(); + Output.WriteLine(actual); + + var expect = @"SELECT + a.sale_id, + a.product_name, + a.quantity, + a.unit_price + FROM + sale AS a"; + + Assert.Equal(expect, actual, true, true, true); + }*/ + public record sale( int? sale_id, string product_name, From d5b8b68428db24d14f9699c0edc800af001edd08 Mon Sep 17 00:00:00 2001 From: mk3008 Date: Thu, 16 May 2024 20:50:52 +0900 Subject: [PATCH 05/23] SelectQuery class specification changes If SelectClause is not specified, all columns are assumed to be selected. --- src/Carbunql/SelectQuery.cs | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/src/Carbunql/SelectQuery.cs b/src/Carbunql/SelectQuery.cs index 997eb4e4..1ff6a08f 100644 --- a/src/Carbunql/SelectQuery.cs +++ b/src/Carbunql/SelectQuery.cs @@ -110,8 +110,6 @@ public override IEnumerable GetCurrentTokens(Token? parent) } } - if (SelectClause == null) yield break; - if (parent == null && WithClause != null) { var commonTables = GetCommonTables(); @@ -121,9 +119,20 @@ public override IEnumerable GetCurrentTokens(Token? parent) } } - foreach (var item in SelectClause.GetTokens(parent)) + if (SelectClause == null) + { + // If SelectClause is not specified, + // all columns are assumed to be selected. + var clause = Token.Reserved(this, parent, "select"); + yield return clause; + yield return new Token(this, clause, "*"); + } + else { - yield return item; + foreach (var item in SelectClause.GetTokens(parent)) + { + yield return item; + } } if (FromClause == null) yield break; From 3bd0e965aefe7ef5ad7b9d384fb7e8de0114ad4f Mon Sep 17 00:00:00 2001 From: mk3008 Date: Thu, 16 May 2024 20:59:36 +0900 Subject: [PATCH 06/23] Organizing code Separate code into classes --- src/Carbunql.TypeSafe/ExpressionExtension.cs | 22 + src/Carbunql.TypeSafe/FluentSelectQuery.cs | 416 +++++++++++++++++ src/Carbunql.TypeSafe/ITableRowDefinition.cs | 10 + src/Carbunql.TypeSafe/Sql.cs | 454 ------------------- 4 files changed, 448 insertions(+), 454 deletions(-) create mode 100644 src/Carbunql.TypeSafe/ExpressionExtension.cs create mode 100644 src/Carbunql.TypeSafe/FluentSelectQuery.cs create mode 100644 src/Carbunql.TypeSafe/ITableRowDefinition.cs diff --git a/src/Carbunql.TypeSafe/ExpressionExtension.cs b/src/Carbunql.TypeSafe/ExpressionExtension.cs new file mode 100644 index 00000000..ff767a63 --- /dev/null +++ b/src/Carbunql.TypeSafe/ExpressionExtension.cs @@ -0,0 +1,22 @@ +using System.Linq.Expressions; + +namespace Carbunql.TypeSafe; + +internal static class ExpressionExtension +{ + internal static object? CompileAndInvoke(this NewExpression exp) + { + var delegateType = typeof(Func<>).MakeGenericType(exp.Type); + var lambda = Expression.Lambda(delegateType, exp); + var compiledLambda = lambda.Compile(); + return compiledLambda.DynamicInvoke(); + } + + internal static object? CompileAndInvoke(this MemberExpression exp) + { + var delegateType = typeof(Func<>).MakeGenericType(exp.Type); + var lambda = Expression.Lambda(delegateType, exp); + var compiledLambda = lambda.Compile(); + return compiledLambda.DynamicInvoke(); + } +} \ No newline at end of file diff --git a/src/Carbunql.TypeSafe/FluentSelectQuery.cs b/src/Carbunql.TypeSafe/FluentSelectQuery.cs new file mode 100644 index 00000000..7dff1368 --- /dev/null +++ b/src/Carbunql.TypeSafe/FluentSelectQuery.cs @@ -0,0 +1,416 @@ +using Carbunql.Analysis.Parser; +using Carbunql.Building; +using System.Linq.Expressions; + +namespace Carbunql.TypeSafe; + +public class FluentSelectQuery : SelectQuery +{ + public FluentSelectQuery Select(Expression> expression) where T : class + { + var analyzed = ExpressionReader.Analyze(expression); + + var body = (NewExpression)expression.Body; + + /* +* LambdaExpression +NodeType + Lambda +Type + Func`1 +Name + "" +ReturnType + <>f__AnonymousType0`1[System.Int32] +Body + * NewExpression + NodeType + New + Type + <>f__AnonymousType0`1 + Arguments.Count + 1 + - index : 0 + * MemberExpression + NodeType + MemberAccess + Type + Int32 + Member + ** MemberInfo + Name + sale_id + MemberType + Property + Expression + * MemberExpression + NodeType + MemberAccess + Type + sale + Member + ** MemberInfo + Name + a + MemberType + Field + Expression + * ConstantExpression + NodeType + Constant + Type + <>c__DisplayClass4_0 + Value + Carbunql.TypeSafe.Test.SingleTableTest+<>c__DisplayClass4_0 + Members.Count + 1 + - index : 0 + ** MemberInfo + Name + id + MemberType + Property +Parameters count + 0 + */ + + var parameterCount = this.GetParameters().Count(); + Func addParameter = (obj) => + { + var pname = $"{DbmsConfiguration.PlaceholderIdentifier}p{parameterCount}"; + parameterCount++; + AddParameter(pname, obj); + return pname; + }; + + if (body.Members != null) + { + var cnt = body.Members.Count(); + for (var i = 0; i < cnt; i++) + { + var alias = body.Members[i].Name; + + if (TryToValue(body.Arguments[i], addParameter, out var value)) + { + this.Select(value).As(alias); + } + + } + } + + return this; + } + + public string ToValue(Expression exp, Func addParameter) + { + if (TryToValue(exp, addParameter, out var value)) + { + return value; + } + throw new Exception(); + } + + public bool TryToValue(Expression exp, Func addParameter, out string value) + { + value = string.Empty; + + //var type = exp.Type; + + Func fn = (obj, tp) => + { + if (obj == null) + { + return "null"; + } + else if (tp == typeof(string)) + { + if (string.IsNullOrEmpty(obj.ToString())) + { + return "''"; + } + else + { + return addParameter(obj); + } + } + else if (tp == typeof(DateTime)) + { + return addParameter(obj); + } + else + { + //var dbtype = DbmsConfiguration.ToDbType(tp); + //return $"cast({obj} as {dbtype})"; + return obj!.ToString()!; + } + }; + + if (exp is MemberExpression mem) + { + var tp = mem.Member.DeclaringType; + + if (tp == typeof(Sql)) + { + if (mem.Member.Name == nameof(Sql.Now)) + { + value = DbmsConfiguration.GetNowCommandLogic(); + return true; + } + if (mem.Member.Name == nameof(Sql.CurrentTimestamp)) + { + value = DbmsConfiguration.GetCurrentTimestampCommandLogic(); + return true; + } + throw new InvalidProgramException(); + } + if (mem.Expression is MemberExpression && typeof(ITableRowDefinition).IsAssignableFrom(tp)) + { + //column + var table = ((MemberExpression)mem.Expression).Member.Name; + var column = mem.Member.Name; + value = $"{table}.{column}"; + return true; + } + else if (mem.Expression is ConstantExpression ce) + { + //variable + var val = mem.CompileAndInvoke(); + value = fn(val, mem.Type); + return true; + } + } + else if (exp is ConstantExpression ce) + { + // static value + value = fn(ce.Value, ce.Type); + return true; + } + else if (exp is NewExpression ne) + { + // ex. new Datetime + value = fn(ne.CompileAndInvoke(), ne.Type); + return true; + } + else if (exp is BinaryExpression be) + { + if (TryToValue(be.Left, addParameter, out var left) && TryToValue(be.Right, addParameter, out var right)) + { + if (be.NodeType == ExpressionType.Coalesce) + { + value = $"{DbmsConfiguration.CoalesceFunctionName}({left}, {right})"; + return true; + } + if (be.NodeType == ExpressionType.Add) + { + value = $"{left} + {right}"; + return true; + } + if (be.NodeType == ExpressionType.Subtract) + { + value = $"{left} - {right}"; + return true; + } + if (be.NodeType == ExpressionType.Multiply) + { + value = $"{left} * {right}"; + return true; + } + if (be.NodeType == ExpressionType.Divide) + { + value = $"{left} / {right}"; + return true; + } + if (be.NodeType == ExpressionType.Modulo) + { + value = DbmsConfiguration.GetModuloCommandLogic(left, right); + return true; + } + if (be.NodeType == ExpressionType.Equal) + { + value = value = $"{left} = {right}"; + return true; + } + if (be.NodeType == ExpressionType.NotEqual) + { + value = value = $"{left} <> {right}"; + return true; + } + if (be.NodeType == ExpressionType.GreaterThan) + { + value = value = $"{left} > {right}"; + return true; + } + if (be.NodeType == ExpressionType.GreaterThanOrEqual) + { + value = value = $"{left} >= {right}"; + return true; + } + if (be.NodeType == ExpressionType.LessThan) + { + value = value = $"{left} < {right}"; + return true; + } + if (be.NodeType == ExpressionType.LessThanOrEqual) + { + value = value = $"{left} <= {right}"; + return true; + } + } + } + else if (exp is UnaryExpression ue) + { + if (TryToValue(ue.Operand, addParameter, out var val)) + { + if (ue.NodeType == ExpressionType.Convert) + { + var dbtype = DbmsConfiguration.ToDbType(ue.Type); + + ////excludes excessive casts + //if (val.StartsWith("cast(")) + //{ + // var fv = (FunctionValue)ValueParser.Parse(val); + // if (fv.Arguments.Count == 1 && fv.Arguments[0] is AsArgument arg && dbtype == arg.Type.ToText()) + // { + // value = val; + // return true; + // } + //} + + value = $"cast({val} as {dbtype})"; + return true; + } + } + } + else if (exp is MethodCallExpression mce) + { + var args = mce.Arguments.Select(x => ToValue(x, addParameter)); + + var tp = mce.Method.DeclaringType?.FullName; + + if (tp == typeof(Math).ToString()) + { + if (mce.Method.Name == nameof(Math.Truncate)) + { + value = DbmsConfiguration.GetTruncateCommandLogic(args); + return true; + } + if (mce.Method.Name == nameof(Math.Floor)) + { + value = DbmsConfiguration.GetFloorCommandLogic(args); + return true; + } + if (mce.Method.Name == nameof(Math.Ceiling)) + { + value = DbmsConfiguration.GetCeilingCommandLogic(args); + return true; + } + if (mce.Method.Name == nameof(Math.Round)) + { + value = DbmsConfiguration.GetRoundCommandLogic(args); + return true; + } + } + if (tp == typeof(Sql).ToString()) + { + //reserved command + if (mce.Method.Name == nameof(Sql.Raw)) + { + var arg = (ConstantExpression)mce.Arguments.First(); + value = arg.Value!.ToString()!; + return true; + } + if (mce.Method.Name == nameof(Sql.RowNumber)) + { + if (mce.Arguments.Count == 0) + { + value = DbmsConfiguration.GetRowNumberCommandLogic(); + return true; + } + if (mce.Arguments.Count == 2) + { + var argList = mce.Arguments.ToList(); + var arg1st = (NewExpression)argList[0]; + var arg2nd = (NewExpression)argList[1]; + var arg1stText = string.Join(",", arg1st.Arguments.Select(x => ToValue(x, addParameter))); + var arg2ndText = string.Join(",", arg2nd.Arguments.Select(x => ToValue(x, addParameter))); + + value = DbmsConfiguration.GetRowNumberPartitionByOrderByCommandLogic(arg1stText, arg2ndText); + return true; + } + } + if (mce.Method.Name == nameof(Sql.RowNumberOrderbyBy)) + { + var arg1st = (NewExpression)mce.Arguments.First(); + + var arg1stText = string.Join(",", arg1st.Arguments.Select(x => ToValue(x, addParameter))); + + value = DbmsConfiguration.GetRowNumberOrderByCommandLogic(arg1stText); + return true; + } + if (mce.Method.Name == nameof(Sql.RowNumberPartitionBy)) + { + var arg1st = (NewExpression)mce.Arguments.First(); + + var arg1stText = string.Join(",", arg1st.Arguments.Select(x => ToValue(x, addParameter))); + + value = DbmsConfiguration.GetRowNumberPartitionByCommandLogic(arg1stText); + return true; + } + throw new InvalidProgramException(); + } + } + else if (exp is ConditionalExpression cnd) + { + var test = ToValue(cnd.Test, addParameter); + var ifTrue = ToValue(cnd.IfTrue, addParameter); + var ifFalse = ToValue(cnd.IfFalse, addParameter); + + if (ifFalse.StartsWith("case ")) + { + var caseExpression = CaseExpressionParser.Parse(ifFalse); + if (caseExpression.CaseCondition is null) + { + var we = WhenExpressionParser.Parse($"when {test} then {ifTrue}"); + caseExpression.WhenExpressions.Insert(0, we); + value = caseExpression.ToText(); + return true; + } + else + { + value = $"case when {test} then {ifTrue} else {ifFalse} end"; + return true; + } + } + else + { + value = $"case when {test} then {ifTrue} else {ifFalse} end"; + return true; + } + } + return false; + } + + //private void Select(Type type, object? value, string alias, Func getParameterName) + //{ + // if (value == null) + // { + // this.Select(new LiteralValue()).As(alias); + // } + // else if (type == typeof(string) || type == typeof(DateTime)) + // { + // var pname = getParameterName(); + // var val = AddParameter(DbmsConfiguration.PlaceholderIdentifier + pname, value); + + // this.Select(val).As(alias); + // } + // else + // { + // var val = new LiteralValue(value.ToString()); + // var dbtype = new LiteralValue(DbmsConfiguration.ToDbType(type)); + // var arg = new AsArgument(val, dbtype); + // var functionValue = new FunctionValue("cast", arg); + + // this.Select(functionValue).As(alias); + // } + //} +} diff --git a/src/Carbunql.TypeSafe/ITableRowDefinition.cs b/src/Carbunql.TypeSafe/ITableRowDefinition.cs new file mode 100644 index 00000000..462e0601 --- /dev/null +++ b/src/Carbunql.TypeSafe/ITableRowDefinition.cs @@ -0,0 +1,10 @@ +using Carbunql.Annotations; +using Carbunql.Clauses; + +namespace Carbunql.TypeSafe; + +public interface ITableRowDefinition +{ + [IgnoreMapping] + TableDefinitionClause TableDefinition { get; set; } +} diff --git a/src/Carbunql.TypeSafe/Sql.cs b/src/Carbunql.TypeSafe/Sql.cs index a43508c3..cc23a559 100644 --- a/src/Carbunql.TypeSafe/Sql.cs +++ b/src/Carbunql.TypeSafe/Sql.cs @@ -1,465 +1,11 @@ using Carbunql.Analysis.Parser; using Carbunql.Annotations; using Carbunql.Building; -using Carbunql.Clauses; -using Carbunql.Values; using System.Data; -using System.Data.Common; using System.Linq.Expressions; namespace Carbunql.TypeSafe; -public interface ITableRowDefinition -{ - [IgnoreMapping] - TableDefinitionClause TableDefinition { get; set; } -} - -public class FluentSelectQuery : SelectQuery -{ - public FluentSelectQuery Select(Expression> expression) where T : class - { - var analyzed = ExpressionReader.Analyze(expression); - - var body = (NewExpression)expression.Body; - - /* -* LambdaExpression -NodeType - Lambda -Type - Func`1 -Name - "" -ReturnType - <>f__AnonymousType0`1[System.Int32] -Body - * NewExpression - NodeType - New - Type - <>f__AnonymousType0`1 - Arguments.Count - 1 - - index : 0 - * MemberExpression - NodeType - MemberAccess - Type - Int32 - Member - ** MemberInfo - Name - sale_id - MemberType - Property - Expression - * MemberExpression - NodeType - MemberAccess - Type - sale - Member - ** MemberInfo - Name - a - MemberType - Field - Expression - * ConstantExpression - NodeType - Constant - Type - <>c__DisplayClass4_0 - Value - Carbunql.TypeSafe.Test.SingleTableTest+<>c__DisplayClass4_0 - Members.Count - 1 - - index : 0 - ** MemberInfo - Name - id - MemberType - Property -Parameters count - 0 - */ - - var parameterCount = this.GetParameters().Count(); - Func addParameter = (obj) => - { - var pname = $"{DbmsConfiguration.PlaceholderIdentifier}p{parameterCount}"; - parameterCount++; - AddParameter(pname, obj); - return pname; - }; - - if (body.Members != null) - { - var cnt = body.Members.Count(); - for (var i = 0; i < cnt; i++) - { - var alias = body.Members[i].Name; - - if (TryToValue(body.Arguments[i], addParameter, out var value)) - { - this.Select(value).As(alias); - } - - } - } - - return this; - } - - - public string ToValue(Expression exp, Func addParameter) - { - if (TryToValue(exp, addParameter, out var value)) - { - return value; - } - throw new Exception(); - } - - public bool TryToValue(Expression exp, Func addParameter, out string value) - { - value = string.Empty; - - //var type = exp.Type; - - Func fn = (obj, tp) => - { - if (obj == null) - { - return "null"; - } - else if (tp == typeof(string)) - { - if (string.IsNullOrEmpty(obj.ToString())) - { - return "''"; - } - else - { - return addParameter(obj); - } - } - else if (tp == typeof(DateTime)) - { - return addParameter(obj); - } - else - { - //var dbtype = DbmsConfiguration.ToDbType(tp); - //return $"cast({obj} as {dbtype})"; - return obj!.ToString()!; - } - }; - - if (exp is MemberExpression mem) - { - var tp = mem.Member.DeclaringType; - - if (tp == typeof(Sql)) - { - if (mem.Member.Name == nameof(Sql.Now)) - { - value = DbmsConfiguration.GetNowCommandLogic(); - return true; - } - if (mem.Member.Name == nameof(Sql.CurrentTimestamp)) - { - value = DbmsConfiguration.GetCurrentTimestampCommandLogic(); - return true; - } - throw new InvalidProgramException(); - } - if (mem.Expression is MemberExpression && typeof(ITableRowDefinition).IsAssignableFrom(tp)) - { - //column - var table = ((MemberExpression)mem.Expression).Member.Name; - var column = mem.Member.Name; - value = $"{table}.{column}"; - return true; - } - else if (mem.Expression is ConstantExpression ce) - { - //variable - var val = mem.CompileAndInvoke(); - value = fn(val, mem.Type); - return true; - } - } - else if (exp is ConstantExpression ce) - { - // static value - value = fn(ce.Value, ce.Type); - return true; - } - else if (exp is NewExpression ne) - { - // ex. new Datetime - value = fn(ne.CompileAndInvoke(), ne.Type); - return true; - } - else if (exp is BinaryExpression be) - { - if (TryToValue(be.Left, addParameter, out var left) && TryToValue(be.Right, addParameter, out var right)) - { - if (be.NodeType == ExpressionType.Coalesce) - { - value = $"{DbmsConfiguration.CoalesceFunctionName}({left}, {right})"; - return true; - } - if (be.NodeType == ExpressionType.Add) - { - value = $"{left} + {right}"; - return true; - } - if (be.NodeType == ExpressionType.Subtract) - { - value = $"{left} - {right}"; - return true; - } - if (be.NodeType == ExpressionType.Multiply) - { - value = $"{left} * {right}"; - return true; - } - if (be.NodeType == ExpressionType.Divide) - { - value = $"{left} / {right}"; - return true; - } - if (be.NodeType == ExpressionType.Modulo) - { - value = DbmsConfiguration.GetModuloCommandLogic(left, right); - return true; - } - if (be.NodeType == ExpressionType.Equal) - { - value = value = $"{left} = {right}"; - return true; - } - if (be.NodeType == ExpressionType.NotEqual) - { - value = value = $"{left} <> {right}"; - return true; - } - if (be.NodeType == ExpressionType.GreaterThan) - { - value = value = $"{left} > {right}"; - return true; - } - if (be.NodeType == ExpressionType.GreaterThanOrEqual) - { - value = value = $"{left} >= {right}"; - return true; - } - if (be.NodeType == ExpressionType.LessThan) - { - value = value = $"{left} < {right}"; - return true; - } - if (be.NodeType == ExpressionType.LessThanOrEqual) - { - value = value = $"{left} <= {right}"; - return true; - } - } - } - else if (exp is UnaryExpression ue) - { - if (TryToValue(ue.Operand, addParameter, out var val)) - { - if (ue.NodeType == ExpressionType.Convert) - { - var dbtype = DbmsConfiguration.ToDbType(ue.Type); - - ////excludes excessive casts - //if (val.StartsWith("cast(")) - //{ - // var fv = (FunctionValue)ValueParser.Parse(val); - // if (fv.Arguments.Count == 1 && fv.Arguments[0] is AsArgument arg && dbtype == arg.Type.ToText()) - // { - // value = val; - // return true; - // } - //} - - value = $"cast({val} as {dbtype})"; - return true; - } - } - } - else if (exp is MethodCallExpression mce) - { - var args = mce.Arguments.Select(x => ToValue(x, addParameter)); - - var tp = mce.Method.DeclaringType?.FullName; - - if (tp == typeof(Math).ToString()) - { - if (mce.Method.Name == nameof(Math.Truncate)) - { - value = DbmsConfiguration.GetTruncateCommandLogic(args); - return true; - } - if (mce.Method.Name == nameof(Math.Floor)) - { - value = DbmsConfiguration.GetFloorCommandLogic(args); - return true; - } - if (mce.Method.Name == nameof(Math.Ceiling)) - { - value = DbmsConfiguration.GetCeilingCommandLogic(args); - return true; - } - if (mce.Method.Name == nameof(Math.Round)) - { - value = DbmsConfiguration.GetRoundCommandLogic(args); - return true; - } - } - if (tp == typeof(Sql).ToString()) - { - //reserved command - if (mce.Method.Name == nameof(Sql.Raw)) - { - var arg = (ConstantExpression)mce.Arguments.First(); - value = arg.Value!.ToString()!; - return true; - } - if (mce.Method.Name == nameof(Sql.RowNumber)) - { - if (mce.Arguments.Count == 0) - { - value = DbmsConfiguration.GetRowNumberCommandLogic(); - return true; - } - if (mce.Arguments.Count == 2) - { - var argList = mce.Arguments.ToList(); - var arg1st = (NewExpression)argList[0]; - var arg2nd = (NewExpression)argList[1]; - var arg1stText = string.Join(",", arg1st.Arguments.Select(x => ToValue(x, addParameter))); - var arg2ndText = string.Join(",", arg2nd.Arguments.Select(x => ToValue(x, addParameter))); - - value = DbmsConfiguration.GetRowNumberPartitionByOrderByCommandLogic(arg1stText, arg2ndText); - return true; - } - } - if (mce.Method.Name == nameof(Sql.RowNumberOrderbyBy)) - { - var arg1st = (NewExpression)mce.Arguments.First(); - - var arg1stText = string.Join(",", arg1st.Arguments.Select(x => ToValue(x, addParameter))); - - value = DbmsConfiguration.GetRowNumberOrderByCommandLogic(arg1stText); - return true; - } - if (mce.Method.Name == nameof(Sql.RowNumberPartitionBy)) - { - var arg1st = (NewExpression)mce.Arguments.First(); - - var arg1stText = string.Join(",", arg1st.Arguments.Select(x => ToValue(x, addParameter))); - - value = DbmsConfiguration.GetRowNumberPartitionByCommandLogic(arg1stText); - return true; - } - throw new InvalidProgramException(); - } - } - else if (exp is ConditionalExpression cnd) - { - var test = ToValue(cnd.Test, addParameter); - var ifTrue = ToValue(cnd.IfTrue, addParameter); - var ifFalse = ToValue(cnd.IfFalse, addParameter); - - if (ifFalse.StartsWith("case ")) - { - var caseExpression = CaseExpressionParser.Parse(ifFalse); - if (caseExpression.CaseCondition is null) - { - var we = WhenExpressionParser.Parse($"when {test} then {ifTrue}"); - caseExpression.WhenExpressions.Insert(0, we); - value = caseExpression.ToText(); - return true; - } - else - { - value = $"case when {test} then {ifTrue} else {ifFalse} end"; - return true; - } - } - else - { - value = $"case when {test} then {ifTrue} else {ifFalse} end"; - return true; - } - } - return false; - } - - //private void Select(Type type, object? value, string alias, Func getParameterName) - //{ - // if (value == null) - // { - // this.Select(new LiteralValue()).As(alias); - // } - // else if (type == typeof(string) || type == typeof(DateTime)) - // { - // var pname = getParameterName(); - // var val = AddParameter(DbmsConfiguration.PlaceholderIdentifier + pname, value); - - // this.Select(val).As(alias); - // } - // else - // { - // var val = new LiteralValue(value.ToString()); - // var dbtype = new LiteralValue(DbmsConfiguration.ToDbType(type)); - // var arg = new AsArgument(val, dbtype); - // var functionValue = new FunctionValue("cast", arg); - - // this.Select(functionValue).As(alias); - // } - //} -} - -internal static class ExpressionExtension -{ - //internal static object? CompileAndInvoke(this MemberExpression exp) - //{ - // var method = typeof(ExpressionExtension) - // .GetMethod(nameof(CompileAndInvokeCore), BindingFlags.NonPublic | BindingFlags.Static)! - // .MakeGenericMethod(exp.Type); - - // return method.Invoke(null, new object[] { exp }); - //} - - //internal static T CompileAndInvokeCore(this MemberExpression exp) - //{ - // var lm = Expression.Lambda>(exp); - // return lm.Compile().Invoke(); - //} - - internal static object? CompileAndInvoke(this NewExpression exp) - { - var delegateType = typeof(Func<>).MakeGenericType(exp.Type); - var lambda = Expression.Lambda(delegateType, exp); - var compiledLambda = lambda.Compile(); - return compiledLambda.DynamicInvoke(); - } - - internal static object? CompileAndInvoke(this MemberExpression exp) - { - var delegateType = typeof(Func<>).MakeGenericType(exp.Type); - var lambda = Expression.Lambda(delegateType, exp); - var compiledLambda = lambda.Compile(); - return compiledLambda.DynamicInvoke(); - } -} /// /// Only function definitions are written for use in expression trees. From 30452227f6af5a4a74af6d1d0f318de1b2de971a Mon Sep 17 00:00:00 2001 From: mk3008 Date: Thu, 16 May 2024 21:24:54 +0900 Subject: [PATCH 07/23] Added type-safe build function for Where clause Logical operator correspondence --- src/Carbunql.TypeSafe/FluentSelectQuery.cs | 110 +++++------- .../Carbunql.TypeSafe.Test/SingleTableTest.cs | 19 +- test/Carbunql.TypeSafe.Test/WhereTest.cs | 164 ++++++++++++++++++ 3 files changed, 228 insertions(+), 65 deletions(-) create mode 100644 test/Carbunql.TypeSafe.Test/WhereTest.cs diff --git a/src/Carbunql.TypeSafe/FluentSelectQuery.cs b/src/Carbunql.TypeSafe/FluentSelectQuery.cs index 7dff1368..463100fe 100644 --- a/src/Carbunql.TypeSafe/FluentSelectQuery.cs +++ b/src/Carbunql.TypeSafe/FluentSelectQuery.cs @@ -8,72 +8,12 @@ public class FluentSelectQuery : SelectQuery { public FluentSelectQuery Select(Expression> expression) where T : class { +#if DEBUG var analyzed = ExpressionReader.Analyze(expression); +#endif var body = (NewExpression)expression.Body; - /* -* LambdaExpression -NodeType - Lambda -Type - Func`1 -Name - "" -ReturnType - <>f__AnonymousType0`1[System.Int32] -Body - * NewExpression - NodeType - New - Type - <>f__AnonymousType0`1 - Arguments.Count - 1 - - index : 0 - * MemberExpression - NodeType - MemberAccess - Type - Int32 - Member - ** MemberInfo - Name - sale_id - MemberType - Property - Expression - * MemberExpression - NodeType - MemberAccess - Type - sale - Member - ** MemberInfo - Name - a - MemberType - Field - Expression - * ConstantExpression - NodeType - Constant - Type - <>c__DisplayClass4_0 - Value - Carbunql.TypeSafe.Test.SingleTableTest+<>c__DisplayClass4_0 - Members.Count - 1 - - index : 0 - ** MemberInfo - Name - id - MemberType - Property -Parameters count - 0 - */ - var parameterCount = this.GetParameters().Count(); Func addParameter = (obj) => { @@ -101,7 +41,36 @@ Parameters count return this; } - public string ToValue(Expression exp, Func addParameter) + public FluentSelectQuery Where(Expression> expression) + { +#if DEBUG + var analyzed = ExpressionReader.Analyze(expression); +#endif + + var body = (BinaryExpression)expression.Body; + + var parameterCount = this.GetParameters().Count(); + Func addParameter = (obj) => + { + var pname = $"{DbmsConfiguration.PlaceholderIdentifier}p{parameterCount}"; + parameterCount++; + AddParameter(pname, obj); + return pname; + }; + + var value = ToValue(body, addParameter); + if (body.NodeType == ExpressionType.OrElse) + { + this.Where($"({ToValue(body, addParameter)})"); + } + else + { + this.Where(ToValue(body, addParameter)); + } + return this; + } + + private string ToValue(Expression exp, Func addParameter) { if (TryToValue(exp, addParameter, out var value)) { @@ -110,7 +79,7 @@ public string ToValue(Expression exp, Func addParameter) throw new Exception(); } - public bool TryToValue(Expression exp, Func addParameter, out string value) + private bool TryToValue(Expression exp, Func addParameter, out string value) { value = string.Empty; @@ -255,6 +224,16 @@ public bool TryToValue(Expression exp, Func addParameter, out s value = value = $"{left} <= {right}"; return true; } + if (be.NodeType == ExpressionType.AndAlso) + { + value = value = $"{left} and {right}"; + return true; + } + if (be.NodeType == ExpressionType.OrElse) + { + value = value = $"({left}) or ({right})"; + return true; + } } } else if (exp is UnaryExpression ue) @@ -387,6 +366,9 @@ public bool TryToValue(Expression exp, Func addParameter, out s return true; } } + + throw new InvalidOperationException(exp.ToString()); + return false; } diff --git a/test/Carbunql.TypeSafe.Test/SingleTableTest.cs b/test/Carbunql.TypeSafe.Test/SingleTableTest.cs index 8a3f0fe7..105e889c 100644 --- a/test/Carbunql.TypeSafe.Test/SingleTableTest.cs +++ b/test/Carbunql.TypeSafe.Test/SingleTableTest.cs @@ -1,5 +1,4 @@ using Carbunql.Clauses; -using System.Diagnostics; using Xunit.Abstractions; namespace Carbunql.TypeSafe.Test; @@ -13,6 +12,24 @@ public SingleTableTest(ITestOutputHelper output) private ITestOutputHelper Output { get; } + [Fact] + public void SelectAllTest() + { + var a = Sql.DefineTable(); + + var query = Sql.From(() => a); + + var actual = query.ToText(); + Output.WriteLine(query.ToText()); + + var expect = @"SELECT + * +FROM + sale AS a"; + + Assert.Equal(expect, actual, true, true, true); + } + [Fact] public void SelectTest() { diff --git a/test/Carbunql.TypeSafe.Test/WhereTest.cs b/test/Carbunql.TypeSafe.Test/WhereTest.cs new file mode 100644 index 00000000..9ea60ed0 --- /dev/null +++ b/test/Carbunql.TypeSafe.Test/WhereTest.cs @@ -0,0 +1,164 @@ +using Carbunql.Clauses; +using Xunit.Abstractions; + +namespace Carbunql.TypeSafe.Test; + +public class WhereTest +{ + public WhereTest(ITestOutputHelper output) + { + Output = output; + } + + private ITestOutputHelper Output { get; } + + [Fact] + public void WhereStaticValue() + { + var a = Sql.DefineTable(); + + var query = Sql.From(() => a) + .Where(() => a.sale_id == 1); + + var actual = query.ToText(); + Output.WriteLine(query.ToText()); + + var expect = @"SELECT + * +FROM + sale AS a +WHERE + a.sale_id = CAST(1 AS integer)"; + + Assert.Equal(expect, actual, true, true, true); + } + + [Fact] + public void WhereStaticVariable() + { + var a = Sql.DefineTable(); + + var id = 1; + + var query = Sql.From(() => a) + .Where(() => a.sale_id == id); + + var actual = query.ToText(); + Output.WriteLine(query.ToText()); + + var expect = @"SELECT + * +FROM + sale AS a +WHERE + a.sale_id = CAST(1 AS integer)"; + + Assert.Equal(expect, actual, true, true, true); + } + + [Fact] + public void Multiple() + { + var a = Sql.DefineTable(); + + var query = Sql.From(() => a) + .Where(() => a.quantity == 1) + .Where(() => a.unit_price == 2); + + var actual = query.ToText(); + Output.WriteLine(query.ToText()); + + var expect = @"SELECT + * +FROM + sale AS a +WHERE + a.quantity = 1 + AND a.unit_price = 2"; + + Assert.Equal(expect, actual, true, true, true); + } + + [Fact] + public void AndTest() + { + var a = Sql.DefineTable(); + + var query = Sql.From(() => a) + .Where(() => a.quantity == 1 && a.unit_price == 2); + + var actual = query.ToText(); + Output.WriteLine(query.ToText()); + + var expect = @"SELECT + * +FROM + sale AS a +WHERE + a.quantity = 1 + AND a.unit_price = 2"; + + Assert.Equal(expect, actual, true, true, true); + } + + [Fact] + public void OrTest() + { + var a = Sql.DefineTable(); + + var query = Sql.From(() => a) + .Where(() => a.quantity == 1 || a.unit_price == 2); + + var actual = query.ToText(); + Output.WriteLine(query.ToText()); + + var expect = @"SELECT + * +FROM + sale AS a +WHERE + ((a.quantity = 1) OR (a.unit_price = 2))"; + + Assert.Equal(expect, actual, true, true, true); + } + + [Fact] + public void BracketTest() + { + var a = Sql.DefineTable(); + + var query = Sql.From(() => a) + .Where(() => + (a.quantity == 1 && a.unit_price == 2) + || + (a.quantity == 10 && a.unit_price == 20) + ); + + var actual = query.ToText(); + Output.WriteLine(query.ToText()); + + var expect = @"SELECT + * +FROM + sale AS a +WHERE + ((a.quantity = 1 AND a.unit_price = 2) OR (a.quantity = 10 AND a.unit_price = 20))"; + + Assert.Equal(expect, actual, true, true, true); + } + + public record sale( + int? sale_id, + string product_name, + int quantity, + decimal unit_price + ) : ITableRowDefinition + { + // no arguments constructor. + // Since it is used as a definition, it has no particular meaning as a value. + public sale() : this(0, "", 0, 0) { } + + // interface property + TableDefinitionClause ITableRowDefinition.TableDefinition { get; set; } = null!; + } +} \ No newline at end of file From 392cb7844eb6906fc2b999e818071e69d9b33122 Mon Sep 17 00:00:00 2001 From: mk3008 Date: Fri, 17 May 2024 20:53:26 +0900 Subject: [PATCH 08/23] Added TypeSafe builder function. Added parentheses analysis processing. --- src/Carbunql.TypeSafe/FluentSelectQuery.cs | 440 +++++++++--------- .../Analysis/Parser/BracketValueParser.cs | 15 + .../Carbunql.TypeSafe.Test/SingleTableTest.cs | 30 +- 3 files changed, 270 insertions(+), 215 deletions(-) diff --git a/src/Carbunql.TypeSafe/FluentSelectQuery.cs b/src/Carbunql.TypeSafe/FluentSelectQuery.cs index 463100fe..b202147e 100644 --- a/src/Carbunql.TypeSafe/FluentSelectQuery.cs +++ b/src/Carbunql.TypeSafe/FluentSelectQuery.cs @@ -1,5 +1,7 @@ using Carbunql.Analysis.Parser; using Carbunql.Building; +using Carbunql.Clauses; +using Carbunql.Values; using System.Linq.Expressions; namespace Carbunql.TypeSafe; @@ -32,9 +34,8 @@ public FluentSelectQuery Select(Expression> expression) where T : cla if (TryToValue(body.Arguments[i], addParameter, out var value)) { - this.Select(value).As(alias); + this.Select(RemoveRootBracketOrDefault(value)).As(alias); } - } } @@ -70,6 +71,19 @@ public FluentSelectQuery Where(Expression> expression) return this; } + private string RemoveRootBracketOrDefault(string value) + { + if (value.StartsWith("(")) + { + //remove excess parentheses + if (BracketValueParser.TryParse(value, out var v) && v is BracketValue bv) + { + return bv.Inner.ToText(); + } + } + return value; + } + private string ToValue(Expression exp, Func addParameter) { if (TryToValue(exp, addParameter, out var value)) @@ -81,10 +95,6 @@ private string ToValue(Expression exp, Func addParameter) private bool TryToValue(Expression exp, Func addParameter, out string value) { - value = string.Empty; - - //var type = exp.Type; - Func fn = (obj, tp) => { if (obj == null) @@ -120,17 +130,9 @@ private bool TryToValue(Expression exp, Func addParameter, out if (tp == typeof(Sql)) { - if (mem.Member.Name == nameof(Sql.Now)) - { - value = DbmsConfiguration.GetNowCommandLogic(); - return true; - } - if (mem.Member.Name == nameof(Sql.CurrentTimestamp)) - { - value = DbmsConfiguration.GetCurrentTimestampCommandLogic(); - return true; - } - throw new InvalidProgramException(); + // ex. Sql.Now, Sql.CurrentTimestamp + value = CreateSqlCommand(mem); + return true; } if (mem.Expression is MemberExpression && typeof(ITableRowDefinition).IsAssignableFrom(tp)) { @@ -140,11 +142,10 @@ private bool TryToValue(Expression exp, Func addParameter, out value = $"{table}.{column}"; return true; } - else if (mem.Expression is ConstantExpression ce) + if (mem.Expression is ConstantExpression ce) { //variable - var val = mem.CompileAndInvoke(); - value = fn(val, mem.Type); + value = fn(mem.CompileAndInvoke(), mem.Type); return true; } } @@ -164,235 +165,248 @@ private bool TryToValue(Expression exp, Func addParameter, out { if (TryToValue(be.Left, addParameter, out var left) && TryToValue(be.Right, addParameter, out var right)) { - if (be.NodeType == ExpressionType.Coalesce) - { - value = $"{DbmsConfiguration.CoalesceFunctionName}({left}, {right})"; - return true; - } - if (be.NodeType == ExpressionType.Add) - { - value = $"{left} + {right}"; - return true; - } - if (be.NodeType == ExpressionType.Subtract) - { - value = $"{left} - {right}"; - return true; - } - if (be.NodeType == ExpressionType.Multiply) - { - value = $"{left} * {right}"; - return true; - } - if (be.NodeType == ExpressionType.Divide) - { - value = $"{left} / {right}"; - return true; - } - if (be.NodeType == ExpressionType.Modulo) - { - value = DbmsConfiguration.GetModuloCommandLogic(left, right); - return true; - } - if (be.NodeType == ExpressionType.Equal) - { - value = value = $"{left} = {right}"; - return true; - } - if (be.NodeType == ExpressionType.NotEqual) - { - value = value = $"{left} <> {right}"; - return true; - } - if (be.NodeType == ExpressionType.GreaterThan) - { - value = value = $"{left} > {right}"; - return true; - } - if (be.NodeType == ExpressionType.GreaterThanOrEqual) - { - value = value = $"{left} >= {right}"; - return true; - } - if (be.NodeType == ExpressionType.LessThan) - { - value = value = $"{left} < {right}"; - return true; - } - if (be.NodeType == ExpressionType.LessThanOrEqual) - { - value = value = $"{left} <= {right}"; - return true; - } - if (be.NodeType == ExpressionType.AndAlso) - { - value = value = $"{left} and {right}"; - return true; - } - if (be.NodeType == ExpressionType.OrElse) - { - value = value = $"({left}) or ({right})"; - return true; - } + value = GetValue(be.NodeType, left, right); + return true; } } else if (exp is UnaryExpression ue) { - if (TryToValue(ue.Operand, addParameter, out var val)) + if (ue.NodeType == ExpressionType.Convert) { - if (ue.NodeType == ExpressionType.Convert) - { - var dbtype = DbmsConfiguration.ToDbType(ue.Type); - - ////excludes excessive casts - //if (val.StartsWith("cast(")) - //{ - // var fv = (FunctionValue)ValueParser.Parse(val); - // if (fv.Arguments.Count == 1 && fv.Arguments[0] is AsArgument arg && dbtype == arg.Type.ToText()) - // { - // value = val; - // return true; - // } - //} - - value = $"cast({val} as {dbtype})"; - return true; - } + value = CreateCastStatement(ue, addParameter); + return true; } + throw new InvalidProgramException(exp.ToString()); } else if (exp is MethodCallExpression mce) { - var args = mce.Arguments.Select(x => ToValue(x, addParameter)); + if (mce.Method.DeclaringType == typeof(Math)) + { + // Math methods like Math.Truncate, Math.Round + value = CreateMathCommand(mce, addParameter); + return true; + } + if (mce.Method.DeclaringType == typeof(Sql)) + { + // Reserved SQL command + value = CreateSqlCommand(mce, addParameter); + return true; + } + throw new InvalidProgramException(exp.ToString()); + } + else if (exp is ConditionalExpression cnd) + { + value = CreateCaseStatement(cnd, addParameter); + return true; + } - var tp = mce.Method.DeclaringType?.FullName; + throw new InvalidOperationException(exp.ToString()); + } - if (tp == typeof(Math).ToString()) + private static string GetValue(ExpressionType nodeType, string left, string right) + { + var opPrecedence = GetPrecedenceFromExpressionType(nodeType); + + var leftValue = ValueParser.Parse(left); + var rightValue = ValueParser.Parse(right); + + // Enclose expressions in parentheses based on operator precedence or specific conditions + if (nodeType == ExpressionType.OrElse) + { + if (leftValue.GetOperators().Any()) { - if (mce.Method.Name == nameof(Math.Truncate)) - { - value = DbmsConfiguration.GetTruncateCommandLogic(args); - return true; - } - if (mce.Method.Name == nameof(Math.Floor)) - { - value = DbmsConfiguration.GetFloorCommandLogic(args); - return true; - } - if (mce.Method.Name == nameof(Math.Ceiling)) - { - value = DbmsConfiguration.GetCeilingCommandLogic(args); - return true; - } - if (mce.Method.Name == nameof(Math.Round)) - { - value = DbmsConfiguration.GetRoundCommandLogic(args); - return true; - } + left = $"({left})"; } - if (tp == typeof(Sql).ToString()) + if (rightValue.GetOperators().Any()) { - //reserved command - if (mce.Method.Name == nameof(Sql.Raw)) + right = $"({right})"; + } + } + else if (opPrecedence == 2) + { + if (leftValue.GetOperators().Any(x => GetOperatorPrecedence(x) < opPrecedence)) + { + left = $"({left})"; + } + if (rightValue.GetOperators().Any(x => GetOperatorPrecedence(x) < opPrecedence)) + { + right = $"({right})"; + } + } + + // Return the formatted expression based on the operation type + return nodeType switch + { + ExpressionType.Coalesce => $"{DbmsConfiguration.CoalesceFunctionName}({left}, {right})", + ExpressionType.Add => $"{left} + {right}", + ExpressionType.Subtract => $"{left} - {right}", + ExpressionType.Multiply => $"{left} * {right}", + ExpressionType.Divide => $"{left} / {right}", + ExpressionType.Modulo => $"{DbmsConfiguration.GetModuloCommandLogic(left, right)}", + ExpressionType.Equal => $"{left} = {right}", + ExpressionType.NotEqual => $"{left} <> {right}", + ExpressionType.GreaterThan => $"{left} > {right}", + ExpressionType.GreaterThanOrEqual => $"{left} >= {right}", + ExpressionType.LessThan => $"{left} < {right}", + ExpressionType.LessThanOrEqual => $"{left} <= {right}", + ExpressionType.AndAlso => $"{left} and {right}", + ExpressionType.OrElse => $"{left} or {right}", + _ => throw new NotSupportedException($"Unsupported expression type: {nodeType}") + }; + } + + private static int GetOperatorPrecedence(string operatorText) + { + return operatorText switch + { + "+" => 1, + "-" => 1, + "*" => 2, + "/" => 2, + _ => 0, + }; + } + + private static int GetPrecedenceFromExpressionType(ExpressionType nodeType) + { + var operatorText = nodeType switch + { + ExpressionType.Add => "+", + ExpressionType.Subtract => "-", + ExpressionType.Multiply => "*", + ExpressionType.Divide => "/", + _ => string.Empty, + }; + return GetOperatorPrecedence(operatorText); + } + + private string CreateMathCommand(MethodCallExpression mce, Func addParameter) + { + var args = mce.Arguments.Select(x => RemoveRootBracketOrDefault(ToValue(x, addParameter))); + + return mce.Method.Name switch + { + nameof(Math.Truncate) => DbmsConfiguration.GetTruncateCommandLogic(args), + nameof(Math.Floor) => DbmsConfiguration.GetFloorCommandLogic(args), + nameof(Math.Ceiling) => DbmsConfiguration.GetCeilingCommandLogic(args), + nameof(Math.Round) => DbmsConfiguration.GetRoundCommandLogic(args), + _ => throw new NotSupportedException($"The method '{mce.Method.Name}' is not supported.") + }; + } + + private string CreateCastStatement(UnaryExpression ue, Func addParameter) + { + var value = ToValue(ue.Operand, addParameter); + var dbtype = DbmsConfiguration.ToDbType(ue.Type); + return $"cast({value} as {dbtype})"; + } + + private string CreateSqlCommand(MemberExpression mem) + { + return mem.Member.Name switch + { + nameof(Sql.Now) => DbmsConfiguration.GetNowCommandLogic(), + nameof(Sql.CurrentTimestamp) => DbmsConfiguration.GetCurrentTimestampCommandLogic(), + _ => throw new NotSupportedException($"The member '{mem.Member.Name}' is not supported.") + }; + } + + private string CreateSqlCommand(MethodCallExpression mce, Func addParameter) + { + switch (mce.Method.Name) + { + case nameof(Sql.Raw): + if (mce.Arguments.First() is ConstantExpression argRaw) { - var arg = (ConstantExpression)mce.Arguments.First(); - value = arg.Value!.ToString()!; - return true; + return argRaw.Value?.ToString() ?? throw new ArgumentException("Raw SQL argument is null."); } - if (mce.Method.Name == nameof(Sql.RowNumber)) + break; + + case nameof(Sql.RowNumber): + if (mce.Arguments.Count == 0) { - if (mce.Arguments.Count == 0) + try { - value = DbmsConfiguration.GetRowNumberCommandLogic(); - return true; + return DbmsConfiguration.GetRowNumberCommandLogic(); } - if (mce.Arguments.Count == 2) + catch (Exception ex) { - var argList = mce.Arguments.ToList(); - var arg1st = (NewExpression)argList[0]; - var arg2nd = (NewExpression)argList[1]; - var arg1stText = string.Join(",", arg1st.Arguments.Select(x => ToValue(x, addParameter))); - var arg2ndText = string.Join(",", arg2nd.Arguments.Select(x => ToValue(x, addParameter))); - - value = DbmsConfiguration.GetRowNumberPartitionByOrderByCommandLogic(arg1stText, arg2ndText); - return true; + throw new InvalidOperationException("Failed to get RowNumber command logic.", ex); } } - if (mce.Method.Name == nameof(Sql.RowNumberOrderbyBy)) + if (mce.Arguments.Count == 2) { - var arg1st = (NewExpression)mce.Arguments.First(); - - var arg1stText = string.Join(",", arg1st.Arguments.Select(x => ToValue(x, addParameter))); + try + { + var argList = mce.Arguments.ToList(); + if (argList[0] is NewExpression arg1st && argList[1] is NewExpression arg2nd) + { + var arg1stText = string.Join(",", arg1st.Arguments.Select(x => ToValue(x, addParameter))); + var arg2ndText = string.Join(",", arg2nd.Arguments.Select(x => ToValue(x, addParameter))); - value = DbmsConfiguration.GetRowNumberOrderByCommandLogic(arg1stText); - return true; + return DbmsConfiguration.GetRowNumberPartitionByOrderByCommandLogic(arg1stText, arg2ndText); + } + } + catch (Exception ex) + { + throw new InvalidOperationException("Failed to process RowNumber with parameters.", ex); + } } - if (mce.Method.Name == nameof(Sql.RowNumberPartitionBy)) - { - var arg1st = (NewExpression)mce.Arguments.First(); + throw new ArgumentException("Invalid arguments count for RowNumber."); - var arg1stText = string.Join(",", arg1st.Arguments.Select(x => ToValue(x, addParameter))); + case nameof(Sql.RowNumberOrderbyBy): + if (mce.Arguments.First() is NewExpression argOrderbyBy) + { + var argOrderbyByText = string.Join(",", argOrderbyBy.Arguments.Select(x => ToValue(x, addParameter))); + return DbmsConfiguration.GetRowNumberOrderByCommandLogic(argOrderbyByText); + } + break; - value = DbmsConfiguration.GetRowNumberPartitionByCommandLogic(arg1stText); - return true; + case nameof(Sql.RowNumberPartitionBy): + if (mce.Arguments.First() is NewExpression argPartitionBy) + { + var argPartitionByText = string.Join(",", argPartitionBy.Arguments.Select(x => ToValue(x, addParameter))); + return DbmsConfiguration.GetRowNumberPartitionByCommandLogic(argPartitionByText); } - throw new InvalidProgramException(); - } + break; + + default: + throw new ArgumentException($"Unsupported method call: {mce.Method.Name}"); } - else if (exp is ConditionalExpression cnd) + + throw new ArgumentException("Invalid argument type for SQL command processing."); + } + + private string CreateCaseStatement(ConditionalExpression cnd, Func addParameter) + { + var test = ToValue(cnd.Test, addParameter); + var ifTrue = ToValue(cnd.IfTrue, addParameter); + var ifFalse = ToValue(cnd.IfFalse, addParameter); + + if (string.IsNullOrEmpty(ifFalse)) { - var test = ToValue(cnd.Test, addParameter); - var ifTrue = ToValue(cnd.IfTrue, addParameter); - var ifFalse = ToValue(cnd.IfFalse, addParameter); + throw new ArgumentException("The IfFalse expression cannot be null or empty.", nameof(cnd.IfFalse)); + } - if (ifFalse.StartsWith("case ")) + // When case statements are nested, check if there is an alternative in the when clause + if (ifFalse.TrimStart().StartsWith("case ", StringComparison.OrdinalIgnoreCase)) + { + var caseExpression = CaseExpressionParser.Parse(ifFalse); + if (caseExpression.CaseCondition is null) { - var caseExpression = CaseExpressionParser.Parse(ifFalse); - if (caseExpression.CaseCondition is null) - { - var we = WhenExpressionParser.Parse($"when {test} then {ifTrue}"); - caseExpression.WhenExpressions.Insert(0, we); - value = caseExpression.ToText(); - return true; - } - else - { - value = $"case when {test} then {ifTrue} else {ifFalse} end"; - return true; - } + // Replace with when clause + var we = WhenExpressionParser.Parse($"when {test} then {ifTrue}"); + caseExpression.WhenExpressions.Insert(0, we); + return caseExpression.ToText(); } else { - value = $"case when {test} then {ifTrue} else {ifFalse} end"; - return true; + return $"case when {test} then {ifTrue} else {ifFalse} end"; } } - - throw new InvalidOperationException(exp.ToString()); - - return false; + else + { + return $"case when {test} then {ifTrue} else {ifFalse} end"; + } } - - //private void Select(Type type, object? value, string alias, Func getParameterName) - //{ - // if (value == null) - // { - // this.Select(new LiteralValue()).As(alias); - // } - // else if (type == typeof(string) || type == typeof(DateTime)) - // { - // var pname = getParameterName(); - // var val = AddParameter(DbmsConfiguration.PlaceholderIdentifier + pname, value); - - // this.Select(val).As(alias); - // } - // else - // { - // var val = new LiteralValue(value.ToString()); - // var dbtype = new LiteralValue(DbmsConfiguration.ToDbType(type)); - // var arg = new AsArgument(val, dbtype); - // var functionValue = new FunctionValue("cast", arg); - - // this.Select(functionValue).As(alias); - // } - //} } diff --git a/src/Carbunql/Analysis/Parser/BracketValueParser.cs b/src/Carbunql/Analysis/Parser/BracketValueParser.cs index b7b42842..65eaa917 100644 --- a/src/Carbunql/Analysis/Parser/BracketValueParser.cs +++ b/src/Carbunql/Analysis/Parser/BracketValueParser.cs @@ -1,6 +1,7 @@ using Carbunql.Clauses; using Carbunql.Extensions; using Carbunql.Values; +using System.Diagnostics.CodeAnalysis; namespace Carbunql.Analysis.Parser; @@ -19,6 +20,20 @@ public static bool IsBracketValue(string text) return text == "("; } + public static bool TryParse(string text, [MaybeNullWhen(false)] out ValueBase value) + { + var r = new SqlTokenReader(text); + var q = Parse(r); + + if (!r.Peek().IsEndToken()) + { + value = null; + return false; + } + value = q; + return true; + } + /// /// Parses a bracket value from the token stream. /// diff --git a/test/Carbunql.TypeSafe.Test/SingleTableTest.cs b/test/Carbunql.TypeSafe.Test/SingleTableTest.cs index 105e889c..585d64b7 100644 --- a/test/Carbunql.TypeSafe.Test/SingleTableTest.cs +++ b/test/Carbunql.TypeSafe.Test/SingleTableTest.cs @@ -114,6 +114,30 @@ public void LiteralTest() Assert.Equal(expect, actual, true, true, true); } + [Fact] + public void BracketTest() + { + var a = Sql.DefineTable(); + + var query = Sql.From(() => a) + .Select(() => new + { + value1 = (a.unit_price + a.unit_price) * 3, + value2 = a.unit_price + (a.unit_price * 3) + }); + + var actual = query.ToText(); + Output.WriteLine(actual); + + var expect = @"SELECT + (a.unit_price + a.unit_price) * 3 AS value1, + a.unit_price + a.unit_price * 3 AS value2 +FROM + sale AS a"; + + Assert.Equal(expect, actual, true, true, true); + } + [Fact] public void VariableTest() { @@ -230,7 +254,8 @@ public void CSharpFunction_Math() v_ceiling = Math.Ceiling(a.unit_price), v_round_arg1 = Math.Round(a.unit_price), v_round_arg2 = Math.Round(a.unit_price, 2), - test = Math.Truncate(a.unit_price * a.quantity), + test1 = Math.Truncate(a.unit_price * a.quantity), + test2 = Math.Truncate(a.unit_price + a.quantity), }); var actual = query.ToText(); @@ -242,7 +267,8 @@ public void CSharpFunction_Math() CEIL(a.unit_price) AS v_ceiling, ROUND(a.unit_price) AS v_round_arg1, ROUND(a.unit_price, 2) AS v_round_arg2, - TRUNC(a.unit_price * CAST(a.quantity AS numeric)) AS test + TRUNC(a.unit_price * CAST(a.quantity AS numeric)) AS test1, + TRUNC(a.unit_price + CAST(a.quantity AS numeric)) AS test2 FROM sale AS a"; From a05a4423b6e646f145bc03d0042db86f28b77303 Mon Sep 17 00:00:00 2001 From: mk3008 Date: Fri, 17 May 2024 21:02:02 +0900 Subject: [PATCH 09/23] Added TypeSafe builder function. Variables are replaced with parameter variables. --- src/Carbunql.TypeSafe/FluentSelectQuery.cs | 6 +++--- .../Carbunql.TypeSafe.Test/SingleTableTest.cs | 20 +++++++++++-------- test/Carbunql.TypeSafe.Test/WhereTest.cs | 7 +++++-- 3 files changed, 20 insertions(+), 13 deletions(-) diff --git a/src/Carbunql.TypeSafe/FluentSelectQuery.cs b/src/Carbunql.TypeSafe/FluentSelectQuery.cs index b202147e..4c8968bf 100644 --- a/src/Carbunql.TypeSafe/FluentSelectQuery.cs +++ b/src/Carbunql.TypeSafe/FluentSelectQuery.cs @@ -62,11 +62,11 @@ public FluentSelectQuery Where(Expression> expression) var value = ToValue(body, addParameter); if (body.NodeType == ExpressionType.OrElse) { - this.Where($"({ToValue(body, addParameter)})"); + this.Where($"({value})"); } else { - this.Where(ToValue(body, addParameter)); + this.Where(value); } return this; } @@ -145,7 +145,7 @@ private bool TryToValue(Expression exp, Func addParameter, out if (mem.Expression is ConstantExpression ce) { //variable - value = fn(mem.CompileAndInvoke(), mem.Type); + value = addParameter(mem.CompileAndInvoke()); return true; } } diff --git a/test/Carbunql.TypeSafe.Test/SingleTableTest.cs b/test/Carbunql.TypeSafe.Test/SingleTableTest.cs index 585d64b7..2bbcf024 100644 --- a/test/Carbunql.TypeSafe.Test/SingleTableTest.cs +++ b/test/Carbunql.TypeSafe.Test/SingleTableTest.cs @@ -165,16 +165,20 @@ public void VariableTest() Output.WriteLine(actual); var expect = @"/* - :p0 = 'test' - :p1 = 2000/01/01 10:10:00 + :p0 = 1 + :p1 = 10 + :p2 = 0.1 + :p3 = True + :p4 = 'test' + :p5 = 2000/01/01 10:10:00 */ SELECT - 1 AS id, - 10 AS value, - 0.1 AS rate, - True AS tf_value, - :p0 AS remarks, - :p1 AS created_at + :p0 AS id, + :p1 AS value, + :p2 AS rate, + :p3 AS tf_value, + :p4 AS remarks, + :p5 AS created_at FROM sale AS a"; diff --git a/test/Carbunql.TypeSafe.Test/WhereTest.cs b/test/Carbunql.TypeSafe.Test/WhereTest.cs index 9ea60ed0..c1399b02 100644 --- a/test/Carbunql.TypeSafe.Test/WhereTest.cs +++ b/test/Carbunql.TypeSafe.Test/WhereTest.cs @@ -46,12 +46,15 @@ public void WhereStaticVariable() var actual = query.ToText(); Output.WriteLine(query.ToText()); - var expect = @"SELECT + var expect = @"/* + :p0 = 1 +*/ +SELECT * FROM sale AS a WHERE - a.sale_id = CAST(1 AS integer)"; + a.sale_id = CAST(:p0 AS integer)"; Assert.Equal(expect, actual, true, true, true); } From a9d3e77fdd22d9387e3d788fee50dc1fb2b9df66 Mon Sep 17 00:00:00 2001 From: mk3008 Date: Fri, 17 May 2024 22:54:32 +0900 Subject: [PATCH 10/23] Added TypeSafe Builder function Addition and subtraction of dates is now supported. --- src/Carbunql.TypeSafe/FluentSelectQuery.cs | 66 ++++++++++++++++++- src/Carbunql.TypeSafe/Sql.cs | 4 +- src/Carbunql/Analysis/Parser/ValueParser.cs | 6 ++ src/Carbunql/Clauses/ValueBase.cs | 1 + src/Carbunql/TokenFormatLogic.cs | 3 + src/Carbunql/Values/Interval.cs | 61 +++++++++++++++++ test/Carbunql.Analysis.Test/ValueParseTest.cs | 14 ++++ .../Carbunql.TypeSafe.Test/SingleTableTest.cs | 28 +++++++- 8 files changed, 177 insertions(+), 6 deletions(-) create mode 100644 src/Carbunql/Values/Interval.cs diff --git a/src/Carbunql.TypeSafe/FluentSelectQuery.cs b/src/Carbunql.TypeSafe/FluentSelectQuery.cs index 4c8968bf..c36b58d0 100644 --- a/src/Carbunql.TypeSafe/FluentSelectQuery.cs +++ b/src/Carbunql.TypeSafe/FluentSelectQuery.cs @@ -2,6 +2,7 @@ using Carbunql.Building; using Carbunql.Clauses; using Carbunql.Values; +using System.Data.Common; using System.Linq.Expressions; namespace Carbunql.TypeSafe; @@ -192,7 +193,9 @@ private bool TryToValue(Expression exp, Func addParameter, out value = CreateSqlCommand(mce, addParameter); return true; } - throw new InvalidProgramException(exp.ToString()); + + value = ParseSqlCommand(exp, addParameter); + return true; } else if (exp is ConditionalExpression cnd) { @@ -203,6 +206,58 @@ private bool TryToValue(Expression exp, Func addParameter, out throw new InvalidOperationException(exp.ToString()); } + private string ParseSqlCommand(Expression expression, Func addParameter) + { + var value = string.Empty; + if (expression is MemberExpression memberExpression) + { + if (memberExpression.Member.DeclaringType == typeof(Sql)) + { + return CreateSqlCommand(memberExpression); + } + throw new NotSupportedException(); + } + + // 現在の式が MethodCallExpression かどうかを確認し、子を探索 + if (expression is MethodCallExpression mce && mce.Object != null) + { + value = ParseSqlCommand(mce.Object, addParameter); + + var arg = ToValue(mce.Arguments[0], addParameter); + if (mce.Method.Name == nameof(DateTime.AddYears)) + { + return $"{value} + {arg} * interval '1 year'"; + } + if (mce.Method.Name == nameof(DateTime.AddMonths)) + { + return $"{value} + {arg} * interval '1 month'"; + } + if (mce.Method.Name == nameof(DateTime.AddDays)) + { + return $"{value} + {arg} * interval '1 day'"; + } + if (mce.Method.Name == nameof(DateTime.AddHours)) + { + return $"{value} + {arg} * interval '1 hour'"; + } + if (mce.Method.Name == nameof(DateTime.AddMinutes)) + { + return $"{value} + {arg} * interval '1 minute'"; + } + if (mce.Method.Name == nameof(DateTime.AddSeconds)) + { + return $"{value} + {arg} * interval '1 second'"; + } + if (mce.Method.Name == nameof(DateTime.AddMilliseconds)) + { + return $"{value} + {arg} * interval '1 ms'"; + } + throw new Exception(); + } + + throw new Exception(); + } + private static string GetValue(ExpressionType nodeType, string left, string right) { var opPrecedence = GetPrecedenceFromExpressionType(nodeType); @@ -297,7 +352,12 @@ private string CreateMathCommand(MethodCallExpression mce, Func private string CreateCastStatement(UnaryExpression ue, Func addParameter) { var value = ToValue(ue.Operand, addParameter); - var dbtype = DbmsConfiguration.ToDbType(ue.Type); + return CreateCastStatement(value, ue.Type); + } + + private string CreateCastStatement(string value, Type type) + { + var dbtype = DbmsConfiguration.ToDbType(type); return $"cast({value} as {dbtype})"; } @@ -305,7 +365,7 @@ private string CreateSqlCommand(MemberExpression mem) { return mem.Member.Name switch { - nameof(Sql.Now) => DbmsConfiguration.GetNowCommandLogic(), + nameof(Sql.Now) => CreateCastStatement(DbmsConfiguration.GetNowCommandLogic(), typeof(DateTime)), nameof(Sql.CurrentTimestamp) => DbmsConfiguration.GetCurrentTimestampCommandLogic(), _ => throw new NotSupportedException($"The member '{mem.Member.Name}' is not supported.") }; diff --git a/src/Carbunql.TypeSafe/Sql.cs b/src/Carbunql.TypeSafe/Sql.cs index cc23a559..0199729b 100644 --- a/src/Carbunql.TypeSafe/Sql.cs +++ b/src/Carbunql.TypeSafe/Sql.cs @@ -42,7 +42,7 @@ public static string Raw(string command) public static string CurrentTimestamp => string.Empty; - public static string Now => string.Empty; + public static DateTime Now => new DateTime(); public static string RowNumber() => string.Empty; @@ -51,4 +51,4 @@ public static string Raw(string command) public static string RowNumberPartitionBy(object partition) => string.Empty; public static string RowNumberOrderbyBy(object order) => string.Empty; -} \ No newline at end of file +} diff --git a/src/Carbunql/Analysis/Parser/ValueParser.cs b/src/Carbunql/Analysis/Parser/ValueParser.cs index 87ec3a32..a759e193 100644 --- a/src/Carbunql/Analysis/Parser/ValueParser.cs +++ b/src/Carbunql/Analysis/Parser/ValueParser.cs @@ -109,6 +109,12 @@ internal static ValueBase ParseCore(ITokenReader r) if (string.IsNullOrEmpty(item)) throw new EndOfStreamException(); + if (item.IsEqualNoCase("interval")) + { + r.Read(); + return new Interval(Parse(r)); + } + if (NegativeValueParser.IsNegativeValue(item)) { return NegativeValueParser.Parse(r); diff --git a/src/Carbunql/Clauses/ValueBase.cs b/src/Carbunql/Clauses/ValueBase.cs index 72c89b9d..196d7d53 100644 --- a/src/Carbunql/Clauses/ValueBase.cs +++ b/src/Carbunql/Clauses/ValueBase.cs @@ -24,6 +24,7 @@ namespace Carbunql.Clauses; [Union(11, typeof(ParameterValue))] [Union(12, typeof(QueryContainer))] [Union(13, typeof(ValueCollection))] +[Union(14, typeof(Interval))] public abstract class ValueBase : IQueryCommandable { /// diff --git a/src/Carbunql/TokenFormatLogic.cs b/src/Carbunql/TokenFormatLogic.cs index abf5a014..f9e06035 100644 --- a/src/Carbunql/TokenFormatLogic.cs +++ b/src/Carbunql/TokenFormatLogic.cs @@ -74,6 +74,8 @@ public virtual bool IsLineBreakOnAfterWriteToken(Token token) if (token.Sender is AlterTableClause) return true; } + + return false; } @@ -93,6 +95,7 @@ public virtual bool IsIncrementIndentOnBeforeWriteToken(Token token) if (token.Parent != null && token.Parent.Sender is ValuesQuery) return false; if (token.Sender is FunctionValue) return false; if (token.Sender is FunctionTable) return false; + if (token.Sender is Interval) return false; if (token.Text.IsEqualNoCase("filter")) return false; if (token.Text.IsEqualNoCase("over")) return false; diff --git a/src/Carbunql/Values/Interval.cs b/src/Carbunql/Values/Interval.cs new file mode 100644 index 00000000..3cefd058 --- /dev/null +++ b/src/Carbunql/Values/Interval.cs @@ -0,0 +1,61 @@ +using Carbunql.Clauses; +using Carbunql.Extensions; +using Carbunql.Tables; +using MessagePack; + +namespace Carbunql.Values; + +[MessagePackObject(keyAsPropertyName: true)] +public class Interval : ValueBase +{ + public Interval(ValueBase argument) + { + Argument = argument; + } + + public ValueBase Argument { get; set; } + + /// + public override IEnumerable GetCurrentTokens(Token? parent) + { + var t = new Token(this, parent, "interval", true); + yield return t; + foreach (var item in Argument.GetTokens(t)) + { + yield return item; + } + } + + /// + protected override IEnumerable GetInternalQueriesCore() + { + foreach (var item in Argument.GetInternalQueries()) + { + yield return item; + } + } + + /// + protected override IEnumerable GetParametersCore() + { + return Argument.GetParameters(); + } + + /// + protected override IEnumerable GetPhysicalTablesCore() + { + foreach (var item in Argument.GetPhysicalTables()) + { + yield return item; + } + } + + /// + protected override IEnumerable GetCommonTablesCore() + { + foreach (var item in Argument.GetCommonTables()) + { + yield return item; + } + } +} diff --git a/test/Carbunql.Analysis.Test/ValueParseTest.cs b/test/Carbunql.Analysis.Test/ValueParseTest.cs index ae618bbc..518cf115 100644 --- a/test/Carbunql.Analysis.Test/ValueParseTest.cs +++ b/test/Carbunql.Analysis.Test/ValueParseTest.cs @@ -753,4 +753,18 @@ public void CatalogSchemaTableColumn() Assert.Equal(text, v.ToText()); } + + [Fact] + public void Interval() + { + var text = @"interval '1 year'"; + + var v = ValueParser.Parse(text); + Monitor.Log(v); + + var lst = v.GetTokens().ToList(); + Assert.Equal(2, lst.Count); + + Assert.Equal(text, v.ToText()); + } } \ No newline at end of file diff --git a/test/Carbunql.TypeSafe.Test/SingleTableTest.cs b/test/Carbunql.TypeSafe.Test/SingleTableTest.cs index 2bbcf024..76555d3b 100644 --- a/test/Carbunql.TypeSafe.Test/SingleTableTest.cs +++ b/test/Carbunql.TypeSafe.Test/SingleTableTest.cs @@ -245,6 +245,32 @@ public void CSharpFunction_Operator() Assert.Equal(expect, actual, true, true, true); } + [Fact] + public void DatetimeTest() + { + var a = Sql.DefineTable(); + + var query = Sql.From(() => a) + .Select(() => new + { + v_now = Sql.Now, + v_add_month = Sql.Now.AddMonths(1), + v_bridge = Sql.Now.AddYears(1).AddMonths(1).AddDays(-1).AddHours(1).AddMinutes(1).AddSeconds(1).AddMilliseconds(1), + }); + + var actual = query.ToText(); + Output.WriteLine(actual); + + var expect = @"SELECT + CAST(NOW() AS timestamp) AS v_now, + CAST(NOW() AS timestamp) + 1 * INTERVAL '1 month' AS v_add_month, + CAST(NOW() AS timestamp) + 1 * INTERVAL '1 year' + 1 * INTERVAL '1 month' + -1 * INTERVAL '1 day' + 1 * INTERVAL '1 hour' + 1 * INTERVAL '1 minute' + 1 * INTERVAL '1 second' + 1 * INTERVAL '1 ms' AS v_bridge +FROM + sale AS a"; + + Assert.Equal(expect, actual, true, true, true); + } + [Fact] public void CSharpFunction_Math() { @@ -321,7 +347,7 @@ public void ReservedCommand() Output.WriteLine(actual); var expect = @"SELECT - NOW() AS now_command, + CAST(NOW() AS timestamp) AS now_command, current_timestamp AS timestamp_commend, ROW_NUMBER() OVER() AS row_num, ROW_NUMBER() OVER( From f89a098ac6f11da32d85d2c75f09d1a6997e7b5e Mon Sep 17 00:00:00 2001 From: mk3008 Date: Fri, 17 May 2024 23:23:42 +0900 Subject: [PATCH 11/23] Added TypeSafe builder function Added date truncation function. --- src/Carbunql.TypeSafe/FluentSelectQuery.cs | 35 ++++++++++++++++++- src/Carbunql.TypeSafe/Sql.cs | 14 ++++++++ .../Carbunql.TypeSafe.Test/SingleTableTest.cs | 27 ++++++++++++-- 3 files changed, 72 insertions(+), 4 deletions(-) diff --git a/src/Carbunql.TypeSafe/FluentSelectQuery.cs b/src/Carbunql.TypeSafe/FluentSelectQuery.cs index c36b58d0..875fba4c 100644 --- a/src/Carbunql.TypeSafe/FluentSelectQuery.cs +++ b/src/Carbunql.TypeSafe/FluentSelectQuery.cs @@ -17,12 +17,24 @@ public FluentSelectQuery Select(Expression> expression) where T : cla var body = (NewExpression)expression.Body; + var parameterDictionary = new Dictionary(); var parameterCount = this.GetParameters().Count(); Func addParameter = (obj) => { + if (obj != null && parameterDictionary.ContainsKey(obj)) + { + return parameterDictionary[obj]; + } + var pname = $"{DbmsConfiguration.PlaceholderIdentifier}p{parameterCount}"; parameterCount++; AddParameter(pname, obj); + + if (obj != null) + { + parameterDictionary[obj] = pname; + } + return pname; }; @@ -215,7 +227,7 @@ private string ParseSqlCommand(Expression expression, Func addP { return CreateSqlCommand(memberExpression); } - throw new NotSupportedException(); + return ToValue(memberExpression, addParameter); } // 現在の式が MethodCallExpression かどうかを確認し、子を探索 @@ -375,6 +387,27 @@ private string CreateSqlCommand(MethodCallExpression mce, Func { switch (mce.Method.Name) { + case nameof(Sql.DateTruncYear): + return $"date_trunc('year', {ToValue(mce.Arguments[0], addParameter)})"; + + case nameof(Sql.DateTruncQuarter): + return $"date_trunc('quarter', {ToValue(mce.Arguments[0], addParameter)})"; + + case nameof(Sql.DateTruncMonth): + return $"date_trunc('month', {ToValue(mce.Arguments[0], addParameter)})"; + + case nameof(Sql.DateTruncDay): + return $"date_trunc('day', {ToValue(mce.Arguments[0], addParameter)})"; + + case nameof(Sql.DateTruncHour): + return $"date_trunc('hour', {ToValue(mce.Arguments[0], addParameter)})"; + + case nameof(Sql.DateTruncMinute): + return $"date_trunc('minute', {ToValue(mce.Arguments[0], addParameter)})"; + + case nameof(Sql.DateTruncSecond): + return $"date_trunc('second', {ToValue(mce.Arguments[0], addParameter)})"; + case nameof(Sql.Raw): if (mce.Arguments.First() is ConstantExpression argRaw) { diff --git a/src/Carbunql.TypeSafe/Sql.cs b/src/Carbunql.TypeSafe/Sql.cs index 0199729b..63275bc0 100644 --- a/src/Carbunql.TypeSafe/Sql.cs +++ b/src/Carbunql.TypeSafe/Sql.cs @@ -44,6 +44,20 @@ public static string Raw(string command) public static DateTime Now => new DateTime(); + public static DateTime DateTruncYear(DateTime d) => new DateTime(); + + public static DateTime DateTruncQuarter(DateTime d) => new DateTime(); + + public static DateTime DateTruncMonth(DateTime d) => new DateTime(); + + public static DateTime DateTruncDay(DateTime d) => new DateTime(); + + public static DateTime DateTruncHour(DateTime d) => new DateTime(); + + public static DateTime DateTruncMinute(DateTime d) => new DateTime(); + + public static DateTime DateTruncSecond(DateTime d) => new DateTime(); + public static string RowNumber() => string.Empty; public static string RowNumber(object partition, object order) => string.Empty; diff --git a/test/Carbunql.TypeSafe.Test/SingleTableTest.cs b/test/Carbunql.TypeSafe.Test/SingleTableTest.cs index 76555d3b..126f5801 100644 --- a/test/Carbunql.TypeSafe.Test/SingleTableTest.cs +++ b/test/Carbunql.TypeSafe.Test/SingleTableTest.cs @@ -250,21 +250,42 @@ public void DatetimeTest() { var a = Sql.DefineTable(); + var d = new DateTime(2000, 10, 20); + var query = Sql.From(() => a) .Select(() => new { + v_trunc_year = Sql.DateTruncYear(d), + v_trunc_quarter = Sql.DateTruncQuarter(d), + v_trunc_month = Sql.DateTruncMonth(d), + v_trunc_day = Sql.DateTruncDay(d), + v_trunc_hour = Sql.DateTruncHour(d), + v_trunc_minute = Sql.DateTruncMinute(d), + v_trunc_second = Sql.DateTruncSecond(d), + v_calc = d.AddMonths(1), v_now = Sql.Now, v_add_month = Sql.Now.AddMonths(1), - v_bridge = Sql.Now.AddYears(1).AddMonths(1).AddDays(-1).AddHours(1).AddMinutes(1).AddSeconds(1).AddMilliseconds(1), + v_test = Sql.Now.AddYears(1).AddMonths(1).AddDays(-1).AddHours(1).AddMinutes(1).AddSeconds(1).AddMilliseconds(1), }); var actual = query.ToText(); Output.WriteLine(actual); - var expect = @"SELECT + var expect = @"/* + :p0 = 2000/10/20 0:00:00 +*/ +SELECT + DATE_TRUNC('year', :p0) AS v_trunc_year, + DATE_TRUNC('quarter', :p0) AS v_trunc_quarter, + DATE_TRUNC('month', :p0) AS v_trunc_month, + DATE_TRUNC('day', :p0) AS v_trunc_day, + DATE_TRUNC('hour', :p0) AS v_trunc_hour, + DATE_TRUNC('minute', :p0) AS v_trunc_minute, + DATE_TRUNC('second', :p0) AS v_trunc_second, + :p0 + 1 * INTERVAL '1 month' AS v_calc, CAST(NOW() AS timestamp) AS v_now, CAST(NOW() AS timestamp) + 1 * INTERVAL '1 month' AS v_add_month, - CAST(NOW() AS timestamp) + 1 * INTERVAL '1 year' + 1 * INTERVAL '1 month' + -1 * INTERVAL '1 day' + 1 * INTERVAL '1 hour' + 1 * INTERVAL '1 minute' + 1 * INTERVAL '1 second' + 1 * INTERVAL '1 ms' AS v_bridge + CAST(NOW() AS timestamp) + 1 * INTERVAL '1 year' + 1 * INTERVAL '1 month' + -1 * INTERVAL '1 day' + 1 * INTERVAL '1 hour' + 1 * INTERVAL '1 minute' + 1 * INTERVAL '1 second' + 1 * INTERVAL '1 ms' AS v_test FROM sale AS a"; From e47624befea09229f5b56a5f91db0d6e6c173f18 Mon Sep 17 00:00:00 2001 From: mk3008 Date: Sat, 18 May 2024 00:08:12 +0900 Subject: [PATCH 12/23] Added TypeSafe Builder function Added LIKE condition function. --- src/Carbunql.TypeSafe/FluentSelectQuery.cs | 80 ++++++++++++++++++---- test/Carbunql.TypeSafe.Test/WhereTest.cs | 28 ++++++++ 2 files changed, 94 insertions(+), 14 deletions(-) diff --git a/src/Carbunql.TypeSafe/FluentSelectQuery.cs b/src/Carbunql.TypeSafe/FluentSelectQuery.cs index 875fba4c..607f1dcd 100644 --- a/src/Carbunql.TypeSafe/FluentSelectQuery.cs +++ b/src/Carbunql.TypeSafe/FluentSelectQuery.cs @@ -17,13 +17,17 @@ public FluentSelectQuery Select(Expression> expression) where T : cla var body = (NewExpression)expression.Body; - var parameterDictionary = new Dictionary(); - var parameterCount = this.GetParameters().Count(); + var prms = this.GetParameters().ToList(); + var parameterCount = prms.Count(); Func addParameter = (obj) => { - if (obj != null && parameterDictionary.ContainsKey(obj)) + if (obj != null) { - return parameterDictionary[obj]; + var q = prms.Where(x => x.Value != null && x.Value.Equals(obj)); + if (q.Any()) + { + return q.First().ParameterName; + } } var pname = $"{DbmsConfiguration.PlaceholderIdentifier}p{parameterCount}"; @@ -32,7 +36,7 @@ public FluentSelectQuery Select(Expression> expression) where T : cla if (obj != null) { - parameterDictionary[obj] = pname; + prms.Add(new QueryParameter(pname, obj)); } return pname; @@ -61,27 +65,51 @@ public FluentSelectQuery Where(Expression> expression) var analyzed = ExpressionReader.Analyze(expression); #endif - var body = (BinaryExpression)expression.Body; - - var parameterCount = this.GetParameters().Count(); + var prms = this.GetParameters().ToList(); + var parameterCount = prms.Count(); Func addParameter = (obj) => { + if (obj != null) + { + var q = prms.Where(x => x.Value != null && x.Value.Equals(obj)); + if (q.Any()) + { + return q.First().ParameterName; + } + } + var pname = $"{DbmsConfiguration.PlaceholderIdentifier}p{parameterCount}"; parameterCount++; AddParameter(pname, obj); + + if (obj != null) + { + prms.Add(new QueryParameter(pname, obj)); + } + return pname; }; - var value = ToValue(body, addParameter); - if (body.NodeType == ExpressionType.OrElse) + if (expression.Body is MethodCallExpression mce) { - this.Where($"({value})"); + this.Where(ToValue(mce, addParameter)); + return this; } - else + else if (expression.Body is BinaryExpression be) { - this.Where(value); + var value = ToValue(be, addParameter); + if (be.NodeType == ExpressionType.OrElse) + { + this.Where($"({value})"); + } + else + { + this.Where(value); + } + return this; } - return this; + + throw new Exception(); } private string RemoveRootBracketOrDefault(string value) @@ -264,6 +292,30 @@ private string ParseSqlCommand(Expression expression, Func addP { return $"{value} + {arg} * interval '1 ms'"; } + if (mce.Method.Name == nameof(String.StartsWith)) + { + return $"{value} like {arg} || '%'"; + } + if (mce.Method.Name == nameof(String.Contains)) + { + return $"{value} like '%' || {arg} || '%'"; + } + if (mce.Method.Name == nameof(String.EndsWith)) + { + return $"{value} like {arg} || '%'"; + } + if (mce.Method.Name == nameof(String.TrimStart)) + { + return $"ltrim({value})"; + } + if (mce.Method.Name == nameof(String.Trim)) + { + return $"trim({value})"; + } + if (mce.Method.Name == nameof(String.TrimEnd)) + { + return $"rtrim({value})"; + } throw new Exception(); } diff --git a/test/Carbunql.TypeSafe.Test/WhereTest.cs b/test/Carbunql.TypeSafe.Test/WhereTest.cs index c1399b02..bbaa180b 100644 --- a/test/Carbunql.TypeSafe.Test/WhereTest.cs +++ b/test/Carbunql.TypeSafe.Test/WhereTest.cs @@ -150,6 +150,34 @@ sale AS a Assert.Equal(expect, actual, true, true, true); } + [Fact] + public void LikeTest() + { + var a = Sql.DefineTable(); + + var query = Sql.From(() => a) + .Where(() => a.product_name.StartsWith("a")) + .Where(() => a.product_name.Contains("a")) + .Where(() => a.product_name.EndsWith("a")); + + var actual = query.ToText(); + Output.WriteLine(query.ToText()); + + var expect = @"/* + :p0 = 'a' +*/ +SELECT + * +FROM + sale AS a +WHERE + a.product_name LIKE :p0 || '%' + AND a.product_name LIKE '%' || :p0 || '%' + AND a.product_name LIKE :p0 || '%'"; + + Assert.Equal(expect, actual, true, true, true); + } + public record sale( int? sale_id, string product_name, From 3145429ff249f3872837aab321f2303cec4e7ae4 Mon Sep 17 00:00:00 2001 From: mk3008 Date: Sat, 18 May 2024 00:12:07 +0900 Subject: [PATCH 13/23] Added TypeSafe Builder function Trim processing support. --- src/Carbunql.TypeSafe/FluentSelectQuery.cs | 83 ++++++++++--------- .../Carbunql.TypeSafe.Test/SingleTableTest.cs | 26 ++++++ 2 files changed, 70 insertions(+), 39 deletions(-) diff --git a/src/Carbunql.TypeSafe/FluentSelectQuery.cs b/src/Carbunql.TypeSafe/FluentSelectQuery.cs index 607f1dcd..490a2eaa 100644 --- a/src/Carbunql.TypeSafe/FluentSelectQuery.cs +++ b/src/Carbunql.TypeSafe/FluentSelectQuery.cs @@ -263,47 +263,52 @@ private string ParseSqlCommand(Expression expression, Func addP { value = ParseSqlCommand(mce.Object, addParameter); - var arg = ToValue(mce.Arguments[0], addParameter); - if (mce.Method.Name == nameof(DateTime.AddYears)) + if (mce.Arguments.Count == 1) { - return $"{value} + {arg} * interval '1 year'"; - } - if (mce.Method.Name == nameof(DateTime.AddMonths)) - { - return $"{value} + {arg} * interval '1 month'"; - } - if (mce.Method.Name == nameof(DateTime.AddDays)) - { - return $"{value} + {arg} * interval '1 day'"; - } - if (mce.Method.Name == nameof(DateTime.AddHours)) - { - return $"{value} + {arg} * interval '1 hour'"; - } - if (mce.Method.Name == nameof(DateTime.AddMinutes)) - { - return $"{value} + {arg} * interval '1 minute'"; - } - if (mce.Method.Name == nameof(DateTime.AddSeconds)) - { - return $"{value} + {arg} * interval '1 second'"; - } - if (mce.Method.Name == nameof(DateTime.AddMilliseconds)) - { - return $"{value} + {arg} * interval '1 ms'"; - } - if (mce.Method.Name == nameof(String.StartsWith)) - { - return $"{value} like {arg} || '%'"; - } - if (mce.Method.Name == nameof(String.Contains)) - { - return $"{value} like '%' || {arg} || '%'"; - } - if (mce.Method.Name == nameof(String.EndsWith)) - { - return $"{value} like {arg} || '%'"; + var arg = ToValue(mce.Arguments[0], addParameter); + if (mce.Method.Name == nameof(DateTime.AddYears)) + { + return $"{value} + {arg} * interval '1 year'"; + } + if (mce.Method.Name == nameof(DateTime.AddMonths)) + { + return $"{value} + {arg} * interval '1 month'"; + } + if (mce.Method.Name == nameof(DateTime.AddDays)) + { + return $"{value} + {arg} * interval '1 day'"; + } + if (mce.Method.Name == nameof(DateTime.AddHours)) + { + return $"{value} + {arg} * interval '1 hour'"; + } + if (mce.Method.Name == nameof(DateTime.AddMinutes)) + { + return $"{value} + {arg} * interval '1 minute'"; + } + if (mce.Method.Name == nameof(DateTime.AddSeconds)) + { + return $"{value} + {arg} * interval '1 second'"; + } + if (mce.Method.Name == nameof(DateTime.AddMilliseconds)) + { + return $"{value} + {arg} * interval '1 ms'"; + } + if (mce.Method.Name == nameof(String.StartsWith)) + { + return $"{value} like {arg} || '%'"; + } + if (mce.Method.Name == nameof(String.Contains)) + { + return $"{value} like '%' || {arg} || '%'"; + } + if (mce.Method.Name == nameof(String.EndsWith)) + { + return $"{value} like {arg} || '%'"; + } + throw new NotImplementedException(); } + if (mce.Method.Name == nameof(String.TrimStart)) { return $"ltrim({value})"; diff --git a/test/Carbunql.TypeSafe.Test/SingleTableTest.cs b/test/Carbunql.TypeSafe.Test/SingleTableTest.cs index 126f5801..7ddab087 100644 --- a/test/Carbunql.TypeSafe.Test/SingleTableTest.cs +++ b/test/Carbunql.TypeSafe.Test/SingleTableTest.cs @@ -523,6 +523,32 @@ END AS v_nest Assert.Equal(expect, actual, true, true, true); } + [Fact] + public void TrimTest() + { + var a = Sql.DefineTable(); + + var query = Sql.From(() => a) + .Select(() => new + { + v_start_trim = a.product_name.TrimStart(), + v_trim = a.product_name.Trim(), + v_end_trim = a.product_name.TrimEnd(), + }); + + var actual = query.ToText(); + Output.WriteLine(actual); + + var expect = @"SELECT + LTRIM(a.product_name) AS v_start_trim, + TRIM(a.product_name) AS v_trim, + RTRIM(a.product_name) AS v_end_trim +FROM + sale AS a"; + + Assert.Equal(expect, actual, true, true, true); + } + // //procrastinate // //[Fact] // public void Switch() From 558a2fda136596dce0fe7baf7aff6786bfbb98a6 Mon Sep 17 00:00:00 2001 From: mk3008 Date: Sat, 18 May 2024 00:38:00 +0900 Subject: [PATCH 14/23] Added TypeSafe Builder function Supports analysis of the ToString method. Supports date formatting. --- src/Carbunql.TypeSafe/FluentSelectQuery.cs | 39 +++++++++++++++++++ .../Carbunql.TypeSafe.Test/SingleTableTest.cs | 37 +++++++++++++++++- 2 files changed, 74 insertions(+), 2 deletions(-) diff --git a/src/Carbunql.TypeSafe/FluentSelectQuery.cs b/src/Carbunql.TypeSafe/FluentSelectQuery.cs index 490a2eaa..85962e6a 100644 --- a/src/Carbunql.TypeSafe/FluentSelectQuery.cs +++ b/src/Carbunql.TypeSafe/FluentSelectQuery.cs @@ -265,6 +265,17 @@ private string ParseSqlCommand(Expression expression, Func addP if (mce.Arguments.Count == 1) { + if (mce.Method.Name == nameof(String.ToString)) + { + Func adder = (obj) => + { + var v = ConverToDbDateFormat(obj!.ToString()!); + return addParameter(v); + }; + var fmv = ToValue(mce.Arguments[0], adder); + return $"to_char({value}, {fmv})"; + } + var arg = ToValue(mce.Arguments[0], addParameter); if (mce.Method.Name == nameof(DateTime.AddYears)) { @@ -321,6 +332,10 @@ private string ParseSqlCommand(Expression expression, Func addP { return $"rtrim({value})"; } + if (mce.Method.Name == nameof(String.ToString)) + { + return CreateCastStatement(value, typeof(string)); + } throw new Exception(); } @@ -559,4 +574,28 @@ private string CreateCaseStatement(ConditionalExpression cnd, Func + { + {"yyyy", "YYYY"}, + {"MM", "MM"}, + {"dd", "DD"}, + {"HH", "HH24"}, + {"mm", "MI"}, + {"ss", "SS"}, + {"ffffff", "US"}, + {"fff", "MS"} + }; + + string dbformat = csharpFormat; + + foreach (var pair in replacements) + { + dbformat = dbformat.Replace(pair.Key, pair.Value); + } + + return dbformat; + } } diff --git a/test/Carbunql.TypeSafe.Test/SingleTableTest.cs b/test/Carbunql.TypeSafe.Test/SingleTableTest.cs index 7ddab087..f61edd1b 100644 --- a/test/Carbunql.TypeSafe.Test/SingleTableTest.cs +++ b/test/Carbunql.TypeSafe.Test/SingleTableTest.cs @@ -549,6 +549,38 @@ public void TrimTest() Assert.Equal(expect, actual, true, true, true); } + [Fact] + public void ToStringTest() + { + var a = Sql.DefineTable(); + + var query = Sql.From(() => a) + .Select(() => new + { + product_name = a.product_name.ToString(), + sale_id = a.sale_id.ToString(), + created_at = a.created_at.ToString("yyyy/MM/dd HH:mm:ss.ffffff"), + created_at2 = a.created_at.ToString("yyyy/MM/dd HH:mm:ss.fff"), + }); + + var actual = query.ToText(); + Output.WriteLine(actual); + + var expect = @"/* + :p0 = 'YYYY/MM/DD HH24:MI:SS.US' + :p1 = 'YYYY/MM/DD HH24:MI:SS.MS' +*/ +SELECT + CAST(a.product_name AS text) AS product_name, + CAST(a.sale_id AS text) AS sale_id, + TO_CHAR(a.created_at, :p0) AS created_at, + TO_CHAR(a.created_at, :p1) AS created_at2 +FROM + sale AS a"; + + Assert.Equal(expect, actual, true, true, true); + } + // //procrastinate // //[Fact] // public void Switch() @@ -623,12 +655,13 @@ public record sale( int? sale_id, string product_name, int quantity, - decimal unit_price + decimal unit_price, + DateTime created_at ) : ITableRowDefinition { // no arguments constructor. // Since it is used as a definition, it has no particular meaning as a value. - public sale() : this(0, "", 0, 0) { } + public sale() : this(0, "", 0, 0, DateTime.Now) { } // interface property TableDefinitionClause ITableRowDefinition.TableDefinition { get; set; } = null!; From d46c0d5dfddc1476ffedb27ebb7a7bf87aa5307e Mon Sep 17 00:00:00 2001 From: mk3008 Date: Sun, 19 May 2024 14:36:00 +0900 Subject: [PATCH 15/23] Add MethodCallExpressionExtension --- .../Extensions/DateTimeExtension.cs | 45 +++ .../MethodCallExpressionExtension.cs | 346 ++++++++++++++++++ src/Carbunql.TypeSafe/FluentSelectQuery.cs | 296 ++------------- src/Carbunql.TypeSafe/Sql.cs | 14 +- .../Carbunql.TypeSafe.Test/SingleTableTest.cs | 70 +++- 5 files changed, 484 insertions(+), 287 deletions(-) create mode 100644 src/Carbunql.TypeSafe/Extensions/DateTimeExtension.cs create mode 100644 src/Carbunql.TypeSafe/Extensions/MethodCallExpressionExtension.cs diff --git a/src/Carbunql.TypeSafe/Extensions/DateTimeExtension.cs b/src/Carbunql.TypeSafe/Extensions/DateTimeExtension.cs new file mode 100644 index 00000000..ca90e411 --- /dev/null +++ b/src/Carbunql.TypeSafe/Extensions/DateTimeExtension.cs @@ -0,0 +1,45 @@ +namespace Carbunql.TypeSafe.Extensions; + +public static class DateTimeExtension +{ + public static DateTime TruncateToYear(this DateTime dateTime) + { + return new DateTime(dateTime.Year, 1, 1); + } + + public static DateTime TruncateToQuarter(this DateTime dateTime) + { + int quarter = (dateTime.Month - 1) / 3 + 1; + return new DateTime(dateTime.Year, quarter * 3 - 2, 1); + } + + public static DateTime TruncateToMonth(this DateTime dateTime) + { + return new DateTime(dateTime.Year, dateTime.Month, 1); + } + + public static DateTime TruncateToDay(this DateTime dateTime) + { + return new DateTime(dateTime.Year, dateTime.Month, dateTime.Day); + } + + public static DateTime TruncateToHour(this DateTime dateTime) + { + return new DateTime(dateTime.Year, dateTime.Month, dateTime.Day, dateTime.Hour, 0, 0); + } + + public static DateTime TruncateToMinute(this DateTime dateTime) + { + return new DateTime(dateTime.Year, dateTime.Month, dateTime.Day, dateTime.Hour, dateTime.Minute, 0); + } + + public static DateTime TruncateToSecond(this DateTime dateTime) + { + return new DateTime(dateTime.Year, dateTime.Month, dateTime.Day, dateTime.Hour, dateTime.Minute, dateTime.Second); + } + + public static DateTime ToMonthEndDate(this DateTime dateTime) + { + return new DateTime(dateTime.Year, dateTime.Month, 1).AddMonths(1).AddDays(-1); + } +} diff --git a/src/Carbunql.TypeSafe/Extensions/MethodCallExpressionExtension.cs b/src/Carbunql.TypeSafe/Extensions/MethodCallExpressionExtension.cs new file mode 100644 index 00000000..f2b4096f --- /dev/null +++ b/src/Carbunql.TypeSafe/Extensions/MethodCallExpressionExtension.cs @@ -0,0 +1,346 @@ +using Carbunql.Analysis.Parser; +using Carbunql.Values; +using System.Linq.Expressions; + +namespace Carbunql.TypeSafe.Extensions; + +internal static class MethodCallExpressionExtension +{ + internal static string ToValue(this MethodCallExpression mce + , Func, string> mainConverter + , Func addParameter) + { + // DeclaringType + if (mce.Method.DeclaringType == typeof(Math)) + { + return CreateMathCommand(mce, mainConverter, addParameter); + } + if (mce.Method.DeclaringType == typeof(Sql)) + { + return CreateSqlCommand(mce, mainConverter, addParameter); + } + if (mce.Method.DeclaringType == typeof(DateTimeExtension)) + { + return CreateDateTimeExtensionCommand(mce, mainConverter, addParameter); + } + + // Return Type + if (mce.Type == typeof(string)) + { + return ToStringValue(mce, mainConverter, addParameter); + } + if (mce.Type == typeof(bool)) + { + return ToBoolValue(mce, mainConverter, addParameter); + } + if (mce.Type == typeof(DateTime)) + { + return ToDateTimeValue(mce, mainConverter, addParameter); + } + + throw new NotSupportedException($"Type:{mce.Type}, DeclaringType:{mce.Method.DeclaringType}"); + } + + private static string CreateMathCommand(this MethodCallExpression mce + , Func, string> mainConverter + , Func addParameter) + { + var args = mce.Arguments.Select(x => RemoveRootBracketOrDefault(mainConverter(x, addParameter))); + + return mce.Method.Name switch + { + nameof(Math.Truncate) => DbmsConfiguration.GetTruncateCommandLogic(args), + nameof(Math.Floor) => DbmsConfiguration.GetFloorCommandLogic(args), + nameof(Math.Ceiling) => DbmsConfiguration.GetCeilingCommandLogic(args), + nameof(Math.Round) => DbmsConfiguration.GetRoundCommandLogic(args), + _ => throw new NotSupportedException($"The method '{mce.Method.Name}' is not supported.") + }; + } + + private static string CreateDateTimeExtensionCommand(MethodCallExpression mce + , Func, string> mainConverter + , Func addParameter) + { + switch (mce.Method.Name) + { + case nameof(DateTimeExtension.TruncateToYear): + return $"date_trunc('year', {mainConverter(mce.Arguments[0], addParameter)})"; + + case nameof(DateTimeExtension.TruncateToQuarter): + return $"date_trunc('quarter', {mainConverter(mce.Arguments[0], addParameter)})"; + + case nameof(DateTimeExtension.TruncateToMonth): + return $"date_trunc('month', {mainConverter(mce.Arguments[0], addParameter)})"; + + case nameof(DateTimeExtension.TruncateToDay): + return $"date_trunc('day', {mainConverter(mce.Arguments[0], addParameter)})"; + + case nameof(DateTimeExtension.TruncateToHour): + return $"date_trunc('hour', {mainConverter(mce.Arguments[0], addParameter)})"; + + case nameof(DateTimeExtension.TruncateToMinute): + return $"date_trunc('minute', {mainConverter(mce.Arguments[0], addParameter)})"; + + case nameof(DateTimeExtension.TruncateToSecond): + return $"date_trunc('second', {mainConverter(mce.Arguments[0], addParameter)})"; + + case nameof(DateTimeExtension.ToMonthEndDate): + return $"date_trunc('month', {mainConverter(mce.Arguments[0], addParameter)}) + interval '1 month - 1 day'"; + + default: + throw new ArgumentException($"Unsupported method call: {mce.Method.Name}"); + } + + throw new ArgumentException("Invalid argument type for SQL command processing."); + } + + private static string CreateSqlCommand(this MethodCallExpression mce + , Func, string> mainConverter + , Func addParameter) + { + switch (mce.Method.Name) + { + case nameof(Sql.DateTruncateToYear): + return $"date_trunc('year', {mainConverter(mce.Arguments[0], addParameter)})"; + + case nameof(Sql.DateTruncateToQuarter): + return $"date_trunc('quarter', {mainConverter(mce.Arguments[0], addParameter)})"; + + case nameof(Sql.DateTruncToMonth): + return $"date_trunc('month', {mainConverter(mce.Arguments[0], addParameter)})"; + + case nameof(Sql.DateTruncateToDay): + return $"date_trunc('day', {mainConverter(mce.Arguments[0], addParameter)})"; + + case nameof(Sql.DateTruncateToHour): + return $"date_trunc('hour', {mainConverter(mce.Arguments[0], addParameter)})"; + + case nameof(Sql.DateTruncateToMinute): + return $"date_trunc('minute', {mainConverter(mce.Arguments[0], addParameter)})"; + + case nameof(Sql.DateTruncateToSecond): + return $"date_trunc('second', {mainConverter(mce.Arguments[0], addParameter)})"; + + case nameof(Sql.Raw): + if (mce.Arguments.First() is ConstantExpression argRaw) + { + return argRaw.Value?.ToString() ?? throw new ArgumentException("Raw SQL argument is null."); + } + break; + + case nameof(Sql.RowNumber): + if (mce.Arguments.Count == 0) + { + try + { + return DbmsConfiguration.GetRowNumberCommandLogic(); + } + catch (Exception ex) + { + throw new InvalidOperationException("Failed to get RowNumber command logic.", ex); + } + } + if (mce.Arguments.Count == 2) + { + try + { + var argList = mce.Arguments.ToList(); + if (argList[0] is NewExpression arg1st && argList[1] is NewExpression arg2nd) + { + var arg1stText = string.Join(",", arg1st.Arguments.Select(x => mainConverter(x, addParameter))); + var arg2ndText = string.Join(",", arg2nd.Arguments.Select(x => mainConverter(x, addParameter))); + + return DbmsConfiguration.GetRowNumberPartitionByOrderByCommandLogic(arg1stText, arg2ndText); + } + } + catch (Exception ex) + { + throw new InvalidOperationException("Failed to process RowNumber with parameters.", ex); + } + } + throw new ArgumentException("Invalid arguments count for RowNumber."); + + case nameof(Sql.RowNumberOrderbyBy): + if (mce.Arguments.First() is NewExpression argOrderbyBy) + { + var argOrderbyByText = string.Join(",", argOrderbyBy.Arguments.Select(x => mainConverter(x, addParameter))); + return DbmsConfiguration.GetRowNumberOrderByCommandLogic(argOrderbyByText); + } + break; + + case nameof(Sql.RowNumberPartitionBy): + if (mce.Arguments.First() is NewExpression argPartitionBy) + { + var argPartitionByText = string.Join(",", argPartitionBy.Arguments.Select(x => mainConverter(x, addParameter))); + return DbmsConfiguration.GetRowNumberPartitionByCommandLogic(argPartitionByText); + } + break; + + default: + throw new ArgumentException($"Unsupported method call: {mce.Method.Name}"); + } + + throw new ArgumentException("Invalid argument type for SQL command processing."); + } + + private static string ToStringValue(this MethodCallExpression mce + , Func, string> mainConverter + , Func addParameter) + { + if (mce.Object != null) + { + var value = mainConverter(mce.Object, addParameter); + if (mce.Arguments.Count == 0) + { + if (mce.Method.Name == nameof(String.TrimStart)) + { + return $"ltrim({value})"; + } + if (mce.Method.Name == nameof(String.Trim)) + { + return $"trim({value})"; + } + if (mce.Method.Name == nameof(String.TrimEnd)) + { + return $"rtrim({value})"; + } + if (mce.Method.Name == nameof(String.ToString)) + { + return FluentSelectQuery.CreateCastStatement(value, typeof(string)); + } + } + + if (mce.Arguments.Count == 1) + { + if (mce.Method.Name == nameof(String.ToString)) + { + Func typeCaster = (obj) => + { + var v = FluentSelectQuery.ConverToDbDateFormat(obj!.ToString()!); + return addParameter(v); + }; + var typedArg = mainConverter(mce.Arguments[0], typeCaster); + return $"to_char({value}, {typedArg})"; + } + + var arg = mainConverter(mce.Arguments[0], addParameter); + if (mce.Method.Name == nameof(String.StartsWith)) + { + return $"{value} like {arg} || '%'"; + } + if (mce.Method.Name == nameof(String.Contains)) + { + return $"{value} like '%' || {arg} || '%'"; + } + if (mce.Method.Name == nameof(String.EndsWith)) + { + return $"{value} like {arg} || '%'"; + } + } + + throw new NotSupportedException($"Object:{mce.Object.Type.FullName}, Method:{mce.Method.Name}, Arguments:{mce.Arguments.Count}, Type:{mce.Type}"); + } + + throw new NotSupportedException($"Object:NULL, Method:{mce.Method.Name}, Arguments:{mce.Arguments.Count}, Type:{mce.Type}"); + } + + private static string ToBoolValue(this MethodCallExpression mce + , Func, string> mainConverter + , Func addParameter) + { + if (mce.Object != null) + { + var value = mainConverter(mce.Object, addParameter); + + if (mce.Arguments.Count == 1) + { + var arg = mainConverter(mce.Arguments[0], addParameter); + if (mce.Method.Name == nameof(String.StartsWith)) + { + return $"{value} like {arg} || '%'"; + } + if (mce.Method.Name == nameof(String.Contains)) + { + return $"{value} like '%' || {arg} || '%'"; + } + if (mce.Method.Name == nameof(String.EndsWith)) + { + return $"{value} like {arg} || '%'"; + } + } + + throw new NotSupportedException($"Object:{mce.Object.Type.FullName}, Method:{mce.Method.Name}, Arguments:{mce.Arguments.Count}, Type:{mce.Type}"); + } + + throw new NotSupportedException($"Object:NULL, Method:{mce.Method.Name}, Arguments:{mce.Arguments.Count}, Type:{mce.Type}"); + } + + private static string ToDateTimeValue(this MethodCallExpression mce + , Func, string> mainConverter + , Func addParameter) + { + if (mce!.Object != null) + { + var value = mainConverter(mce!.Object!, addParameter); + + if (mce.Arguments.Count == 1) + { + var arg = mainConverter(mce.Arguments[0], addParameter); + + if (mce.Method.Name == nameof(DateTime.AddYears)) + { + return $"{value} + {arg} * interval '1 year'"; + } + if (mce.Method.Name == nameof(DateTime.AddMonths)) + { + return $"{value} + {arg} * interval '1 month'"; + } + if (mce.Method.Name == nameof(DateTime.AddDays)) + { + return $"{value} + {arg} * interval '1 day'"; + } + if (mce.Method.Name == nameof(DateTime.AddHours)) + { + return $"{value} + {arg} * interval '1 hour'"; + } + if (mce.Method.Name == nameof(DateTime.AddMinutes)) + { + return $"{value} + {arg} * interval '1 minute'"; + } + if (mce.Method.Name == nameof(DateTime.AddSeconds)) + { + return $"{value} + {arg} * interval '1 second'"; + } + if (mce.Method.Name == nameof(DateTime.AddMilliseconds)) + { + return $"{value} + {arg} * interval '1 ms'"; + } + } + + throw new NotSupportedException($"Object:{mce.Object.Type.FullName}, Method:{mce.Method.Name}, Arguments:{mce.Arguments.Count}, Type:{mce.Type}"); + } + else + { + if (mce.Arguments.Count == 1) + { + Func echo = x => x!.ToString()!; + return mce.ToValue(mainConverter, echo); + } + + throw new NotSupportedException($"Object:NULL, Method:{mce.Method.Name}, Arguments:{mce.Arguments.Count}, Type:{mce.Type}"); + } + } + + private static string RemoveRootBracketOrDefault(string value) + { + if (value.StartsWith("(")) + { + //remove excess parentheses + if (BracketValueParser.TryParse(value, out var v) && v is BracketValue bv) + { + return bv.Inner.ToText(); + } + } + return value; + } +} diff --git a/src/Carbunql.TypeSafe/FluentSelectQuery.cs b/src/Carbunql.TypeSafe/FluentSelectQuery.cs index 85962e6a..e98af691 100644 --- a/src/Carbunql.TypeSafe/FluentSelectQuery.cs +++ b/src/Carbunql.TypeSafe/FluentSelectQuery.cs @@ -1,8 +1,7 @@ using Carbunql.Analysis.Parser; using Carbunql.Building; -using Carbunql.Clauses; +using Carbunql.TypeSafe.Extensions; using Carbunql.Values; -using System.Data.Common; using System.Linq.Expressions; namespace Carbunql.TypeSafe; @@ -48,11 +47,8 @@ public FluentSelectQuery Select(Expression> expression) where T : cla for (var i = 0; i < cnt; i++) { var alias = body.Members[i].Name; - - if (TryToValue(body.Arguments[i], addParameter, out var value)) - { - this.Select(RemoveRootBracketOrDefault(value)).As(alias); - } + var value = ToValue(body.Arguments[i], addParameter); + this.Select(RemoveRootBracketOrDefault(value)).As(alias); } } @@ -112,29 +108,7 @@ public FluentSelectQuery Where(Expression> expression) throw new Exception(); } - private string RemoveRootBracketOrDefault(string value) - { - if (value.StartsWith("(")) - { - //remove excess parentheses - if (BracketValueParser.TryParse(value, out var v) && v is BracketValue bv) - { - return bv.Inner.ToText(); - } - } - return value; - } - private string ToValue(Expression exp, Func addParameter) - { - if (TryToValue(exp, addParameter, out var value)) - { - return value; - } - throw new Exception(); - } - - private bool TryToValue(Expression exp, Func addParameter, out string value) { Func fn = (obj, tp) => { @@ -172,177 +146,59 @@ private bool TryToValue(Expression exp, Func addParameter, out if (tp == typeof(Sql)) { // ex. Sql.Now, Sql.CurrentTimestamp - value = CreateSqlCommand(mem); - return true; + return CreateSqlCommand(mem); } if (mem.Expression is MemberExpression && typeof(ITableRowDefinition).IsAssignableFrom(tp)) { //column var table = ((MemberExpression)mem.Expression).Member.Name; var column = mem.Member.Name; - value = $"{table}.{column}"; - return true; + return $"{table}.{column}"; } if (mem.Expression is ConstantExpression ce) { //variable - value = addParameter(mem.CompileAndInvoke()); - return true; + return addParameter(mem.CompileAndInvoke()); } + throw new InvalidProgramException(exp.ToString()); } else if (exp is ConstantExpression ce) { // static value - value = fn(ce.Value, ce.Type); - return true; + return fn(ce.Value, ce.Type); } else if (exp is NewExpression ne) { // ex. new Datetime - value = fn(ne.CompileAndInvoke(), ne.Type); - return true; + return fn(ne.CompileAndInvoke(), ne.Type); } else if (exp is BinaryExpression be) { - if (TryToValue(be.Left, addParameter, out var left) && TryToValue(be.Right, addParameter, out var right)) - { - value = GetValue(be.NodeType, left, right); - return true; - } + var left = ToValue(be.Left, addParameter); + var right = ToValue(be.Right, addParameter); + return ToValue(be.NodeType, left, right); } else if (exp is UnaryExpression ue) { if (ue.NodeType == ExpressionType.Convert) { - value = CreateCastStatement(ue, addParameter); - return true; + return CreateCastStatement(ue, addParameter); } throw new InvalidProgramException(exp.ToString()); } else if (exp is MethodCallExpression mce) { - if (mce.Method.DeclaringType == typeof(Math)) - { - // Math methods like Math.Truncate, Math.Round - value = CreateMathCommand(mce, addParameter); - return true; - } - if (mce.Method.DeclaringType == typeof(Sql)) - { - // Reserved SQL command - value = CreateSqlCommand(mce, addParameter); - return true; - } - - value = ParseSqlCommand(exp, addParameter); - return true; + return mce.ToValue(ToValue, addParameter); } else if (exp is ConditionalExpression cnd) { - value = CreateCaseStatement(cnd, addParameter); - return true; + return CreateCaseStatement(cnd, addParameter); } - throw new InvalidOperationException(exp.ToString()); + throw new InvalidProgramException(exp.ToString()); } - private string ParseSqlCommand(Expression expression, Func addParameter) - { - var value = string.Empty; - if (expression is MemberExpression memberExpression) - { - if (memberExpression.Member.DeclaringType == typeof(Sql)) - { - return CreateSqlCommand(memberExpression); - } - return ToValue(memberExpression, addParameter); - } - - // 現在の式が MethodCallExpression かどうかを確認し、子を探索 - if (expression is MethodCallExpression mce && mce.Object != null) - { - value = ParseSqlCommand(mce.Object, addParameter); - - if (mce.Arguments.Count == 1) - { - if (mce.Method.Name == nameof(String.ToString)) - { - Func adder = (obj) => - { - var v = ConverToDbDateFormat(obj!.ToString()!); - return addParameter(v); - }; - var fmv = ToValue(mce.Arguments[0], adder); - return $"to_char({value}, {fmv})"; - } - - var arg = ToValue(mce.Arguments[0], addParameter); - if (mce.Method.Name == nameof(DateTime.AddYears)) - { - return $"{value} + {arg} * interval '1 year'"; - } - if (mce.Method.Name == nameof(DateTime.AddMonths)) - { - return $"{value} + {arg} * interval '1 month'"; - } - if (mce.Method.Name == nameof(DateTime.AddDays)) - { - return $"{value} + {arg} * interval '1 day'"; - } - if (mce.Method.Name == nameof(DateTime.AddHours)) - { - return $"{value} + {arg} * interval '1 hour'"; - } - if (mce.Method.Name == nameof(DateTime.AddMinutes)) - { - return $"{value} + {arg} * interval '1 minute'"; - } - if (mce.Method.Name == nameof(DateTime.AddSeconds)) - { - return $"{value} + {arg} * interval '1 second'"; - } - if (mce.Method.Name == nameof(DateTime.AddMilliseconds)) - { - return $"{value} + {arg} * interval '1 ms'"; - } - if (mce.Method.Name == nameof(String.StartsWith)) - { - return $"{value} like {arg} || '%'"; - } - if (mce.Method.Name == nameof(String.Contains)) - { - return $"{value} like '%' || {arg} || '%'"; - } - if (mce.Method.Name == nameof(String.EndsWith)) - { - return $"{value} like {arg} || '%'"; - } - throw new NotImplementedException(); - } - - if (mce.Method.Name == nameof(String.TrimStart)) - { - return $"ltrim({value})"; - } - if (mce.Method.Name == nameof(String.Trim)) - { - return $"trim({value})"; - } - if (mce.Method.Name == nameof(String.TrimEnd)) - { - return $"rtrim({value})"; - } - if (mce.Method.Name == nameof(String.ToString)) - { - return CreateCastStatement(value, typeof(string)); - } - throw new Exception(); - } - - throw new Exception(); - } - - private static string GetValue(ExpressionType nodeType, string left, string right) + private string ToValue(ExpressionType nodeType, string left, string right) { var opPrecedence = GetPrecedenceFromExpressionType(nodeType); @@ -419,27 +275,13 @@ private static int GetPrecedenceFromExpressionType(ExpressionType nodeType) return GetOperatorPrecedence(operatorText); } - private string CreateMathCommand(MethodCallExpression mce, Func addParameter) - { - var args = mce.Arguments.Select(x => RemoveRootBracketOrDefault(ToValue(x, addParameter))); - - return mce.Method.Name switch - { - nameof(Math.Truncate) => DbmsConfiguration.GetTruncateCommandLogic(args), - nameof(Math.Floor) => DbmsConfiguration.GetFloorCommandLogic(args), - nameof(Math.Ceiling) => DbmsConfiguration.GetCeilingCommandLogic(args), - nameof(Math.Round) => DbmsConfiguration.GetRoundCommandLogic(args), - _ => throw new NotSupportedException($"The method '{mce.Method.Name}' is not supported.") - }; - } - private string CreateCastStatement(UnaryExpression ue, Func addParameter) { var value = ToValue(ue.Operand, addParameter); return CreateCastStatement(value, ue.Type); } - private string CreateCastStatement(string value, Type type) + internal static string CreateCastStatement(string value, Type type) { var dbtype = DbmsConfiguration.ToDbType(type); return $"cast({value} as {dbtype})"; @@ -455,93 +297,6 @@ private string CreateSqlCommand(MemberExpression mem) }; } - private string CreateSqlCommand(MethodCallExpression mce, Func addParameter) - { - switch (mce.Method.Name) - { - case nameof(Sql.DateTruncYear): - return $"date_trunc('year', {ToValue(mce.Arguments[0], addParameter)})"; - - case nameof(Sql.DateTruncQuarter): - return $"date_trunc('quarter', {ToValue(mce.Arguments[0], addParameter)})"; - - case nameof(Sql.DateTruncMonth): - return $"date_trunc('month', {ToValue(mce.Arguments[0], addParameter)})"; - - case nameof(Sql.DateTruncDay): - return $"date_trunc('day', {ToValue(mce.Arguments[0], addParameter)})"; - - case nameof(Sql.DateTruncHour): - return $"date_trunc('hour', {ToValue(mce.Arguments[0], addParameter)})"; - - case nameof(Sql.DateTruncMinute): - return $"date_trunc('minute', {ToValue(mce.Arguments[0], addParameter)})"; - - case nameof(Sql.DateTruncSecond): - return $"date_trunc('second', {ToValue(mce.Arguments[0], addParameter)})"; - - case nameof(Sql.Raw): - if (mce.Arguments.First() is ConstantExpression argRaw) - { - return argRaw.Value?.ToString() ?? throw new ArgumentException("Raw SQL argument is null."); - } - break; - - case nameof(Sql.RowNumber): - if (mce.Arguments.Count == 0) - { - try - { - return DbmsConfiguration.GetRowNumberCommandLogic(); - } - catch (Exception ex) - { - throw new InvalidOperationException("Failed to get RowNumber command logic.", ex); - } - } - if (mce.Arguments.Count == 2) - { - try - { - var argList = mce.Arguments.ToList(); - if (argList[0] is NewExpression arg1st && argList[1] is NewExpression arg2nd) - { - var arg1stText = string.Join(",", arg1st.Arguments.Select(x => ToValue(x, addParameter))); - var arg2ndText = string.Join(",", arg2nd.Arguments.Select(x => ToValue(x, addParameter))); - - return DbmsConfiguration.GetRowNumberPartitionByOrderByCommandLogic(arg1stText, arg2ndText); - } - } - catch (Exception ex) - { - throw new InvalidOperationException("Failed to process RowNumber with parameters.", ex); - } - } - throw new ArgumentException("Invalid arguments count for RowNumber."); - - case nameof(Sql.RowNumberOrderbyBy): - if (mce.Arguments.First() is NewExpression argOrderbyBy) - { - var argOrderbyByText = string.Join(",", argOrderbyBy.Arguments.Select(x => ToValue(x, addParameter))); - return DbmsConfiguration.GetRowNumberOrderByCommandLogic(argOrderbyByText); - } - break; - - case nameof(Sql.RowNumberPartitionBy): - if (mce.Arguments.First() is NewExpression argPartitionBy) - { - var argPartitionByText = string.Join(",", argPartitionBy.Arguments.Select(x => ToValue(x, addParameter))); - return DbmsConfiguration.GetRowNumberPartitionByCommandLogic(argPartitionByText); - } - break; - - default: - throw new ArgumentException($"Unsupported method call: {mce.Method.Name}"); - } - - throw new ArgumentException("Invalid argument type for SQL command processing."); - } - private string CreateCaseStatement(ConditionalExpression cnd, Func addParameter) { var test = ToValue(cnd.Test, addParameter); @@ -575,7 +330,7 @@ private string CreateCaseStatement(ConditionalExpression cnd, Func { @@ -598,4 +353,17 @@ static string ConverToDbDateFormat(string csharpFormat) return dbformat; } + + private string RemoveRootBracketOrDefault(string value) + { + if (value.StartsWith("(")) + { + //remove excess parentheses + if (BracketValueParser.TryParse(value, out var v) && v is BracketValue bv) + { + return bv.Inner.ToText(); + } + } + return value; + } } diff --git a/src/Carbunql.TypeSafe/Sql.cs b/src/Carbunql.TypeSafe/Sql.cs index 63275bc0..ea3cc77a 100644 --- a/src/Carbunql.TypeSafe/Sql.cs +++ b/src/Carbunql.TypeSafe/Sql.cs @@ -44,19 +44,19 @@ public static string Raw(string command) public static DateTime Now => new DateTime(); - public static DateTime DateTruncYear(DateTime d) => new DateTime(); + public static DateTime DateTruncateToYear(DateTime d) => new DateTime(); - public static DateTime DateTruncQuarter(DateTime d) => new DateTime(); + public static DateTime DateTruncateToQuarter(DateTime d) => new DateTime(); - public static DateTime DateTruncMonth(DateTime d) => new DateTime(); + public static DateTime DateTruncToMonth(DateTime d) => new DateTime(); - public static DateTime DateTruncDay(DateTime d) => new DateTime(); + public static DateTime DateTruncateToDay(DateTime d) => new DateTime(); - public static DateTime DateTruncHour(DateTime d) => new DateTime(); + public static DateTime DateTruncateToHour(DateTime d) => new DateTime(); - public static DateTime DateTruncMinute(DateTime d) => new DateTime(); + public static DateTime DateTruncateToMinute(DateTime d) => new DateTime(); - public static DateTime DateTruncSecond(DateTime d) => new DateTime(); + public static DateTime DateTruncateToSecond(DateTime d) => new DateTime(); public static string RowNumber() => string.Empty; diff --git a/test/Carbunql.TypeSafe.Test/SingleTableTest.cs b/test/Carbunql.TypeSafe.Test/SingleTableTest.cs index f61edd1b..013872cd 100644 --- a/test/Carbunql.TypeSafe.Test/SingleTableTest.cs +++ b/test/Carbunql.TypeSafe.Test/SingleTableTest.cs @@ -1,4 +1,5 @@ using Carbunql.Clauses; +using Carbunql.TypeSafe.Extensions; using Xunit.Abstractions; namespace Carbunql.TypeSafe.Test; @@ -246,7 +247,7 @@ public void CSharpFunction_Operator() } [Fact] - public void DatetimeTest() + public void DatetimeTest_SqlCommand() { var a = Sql.DefineTable(); @@ -255,17 +256,14 @@ public void DatetimeTest() var query = Sql.From(() => a) .Select(() => new { - v_trunc_year = Sql.DateTruncYear(d), - v_trunc_quarter = Sql.DateTruncQuarter(d), - v_trunc_month = Sql.DateTruncMonth(d), - v_trunc_day = Sql.DateTruncDay(d), - v_trunc_hour = Sql.DateTruncHour(d), - v_trunc_minute = Sql.DateTruncMinute(d), - v_trunc_second = Sql.DateTruncSecond(d), - v_calc = d.AddMonths(1), - v_now = Sql.Now, - v_add_month = Sql.Now.AddMonths(1), - v_test = Sql.Now.AddYears(1).AddMonths(1).AddDays(-1).AddHours(1).AddMinutes(1).AddSeconds(1).AddMilliseconds(1), + v_trunc_year = Sql.DateTruncateToYear(d), + v_trunc_quarter = Sql.DateTruncateToQuarter(d), + v_trunc_month = Sql.DateTruncToMonth(d), + v_trunc_day = Sql.DateTruncateToDay(d), + v_trunc_hour = Sql.DateTruncateToHour(d), + v_trunc_minute = Sql.DateTruncateToMinute(d), + v_trunc_second = Sql.DateTruncateToSecond(d), + last_date_of_month = Sql.DateTruncToMonth(Sql.Now).AddMonths(1).AddDays(-1), }); var actual = query.ToText(); @@ -282,10 +280,50 @@ public void DatetimeTest() DATE_TRUNC('hour', :p0) AS v_trunc_hour, DATE_TRUNC('minute', :p0) AS v_trunc_minute, DATE_TRUNC('second', :p0) AS v_trunc_second, - :p0 + 1 * INTERVAL '1 month' AS v_calc, - CAST(NOW() AS timestamp) AS v_now, - CAST(NOW() AS timestamp) + 1 * INTERVAL '1 month' AS v_add_month, - CAST(NOW() AS timestamp) + 1 * INTERVAL '1 year' + 1 * INTERVAL '1 month' + -1 * INTERVAL '1 day' + 1 * INTERVAL '1 hour' + 1 * INTERVAL '1 minute' + 1 * INTERVAL '1 second' + 1 * INTERVAL '1 ms' AS v_test + DATE_TRUNC('month', CAST(NOW() AS timestamp)) + 1 * INTERVAL '1 month' + -1 * INTERVAL '1 day' AS last_date_of_month +FROM + sale AS a"; + + Assert.Equal(expect, actual, true, true, true); + } + + [Fact] + public void DatetimeTest_SqlExtension() + { + var a = Sql.DefineTable(); + + var d = new DateTime(2000, 10, 20); + + var query = Sql.From(() => a) + .Select(() => new + { + v_trunc_year = d.TruncateToYear(), + v_trunc_quarter = d.TruncateToQuarter(), + v_trunc_month = d.TruncateToMonth(), + v_trunc_day = d.TruncateToDay(), + v_trunc_hour = d.TruncateToHour(), + v_trunc_minute = d.TruncateToMinute(), + v_trunc_second = d.TruncateToSecond(), + last_date_of_month = Sql.Now.TruncateToMonth().AddMonths(1).AddDays(-1), + last_date_of_month2 = Sql.Now.ToMonthEndDate(), + }); + + var actual = query.ToText(); + Output.WriteLine(actual); + + var expect = @"/* + :p0 = 2000/10/20 0:00:00 +*/ +SELECT + DATE_TRUNC('year', :p0) AS v_trunc_year, + DATE_TRUNC('quarter', :p0) AS v_trunc_quarter, + DATE_TRUNC('month', :p0) AS v_trunc_month, + DATE_TRUNC('day', :p0) AS v_trunc_day, + DATE_TRUNC('hour', :p0) AS v_trunc_hour, + DATE_TRUNC('minute', :p0) AS v_trunc_minute, + DATE_TRUNC('second', :p0) AS v_trunc_second, + DATE_TRUNC('month', CAST(NOW() AS timestamp)) + 1 * INTERVAL '1 month' + -1 * INTERVAL '1 day' AS last_date_of_month, + DATE_TRUNC('month', CAST(NOW() AS timestamp)) + INTERVAL '1 month - 1 day' AS last_date_of_month2 FROM sale AS a"; From 5b6ce373d3b8a67db6cb9bf4c6852b6af3c38e85 Mon Sep 17 00:00:00 2001 From: mk3008 Date: Sun, 19 May 2024 14:43:32 +0900 Subject: [PATCH 16/23] Add MemberExpressionExtension --- src/Carbunql.TypeSafe/ExpressionExtension.cs | 8 +-- .../Extensions/MemberExpressionExtension.cs | 51 +++++++++++++++++++ .../MethodCallExpressionExtension.cs | 2 +- src/Carbunql.TypeSafe/FluentSelectQuery.cs | 31 +---------- 4 files changed, 54 insertions(+), 38 deletions(-) create mode 100644 src/Carbunql.TypeSafe/Extensions/MemberExpressionExtension.cs diff --git a/src/Carbunql.TypeSafe/ExpressionExtension.cs b/src/Carbunql.TypeSafe/ExpressionExtension.cs index ff767a63..2c863ed3 100644 --- a/src/Carbunql.TypeSafe/ExpressionExtension.cs +++ b/src/Carbunql.TypeSafe/ExpressionExtension.cs @@ -12,11 +12,5 @@ internal static class ExpressionExtension return compiledLambda.DynamicInvoke(); } - internal static object? CompileAndInvoke(this MemberExpression exp) - { - var delegateType = typeof(Func<>).MakeGenericType(exp.Type); - var lambda = Expression.Lambda(delegateType, exp); - var compiledLambda = lambda.Compile(); - return compiledLambda.DynamicInvoke(); - } + } \ No newline at end of file diff --git a/src/Carbunql.TypeSafe/Extensions/MemberExpressionExtension.cs b/src/Carbunql.TypeSafe/Extensions/MemberExpressionExtension.cs new file mode 100644 index 00000000..52e057b7 --- /dev/null +++ b/src/Carbunql.TypeSafe/Extensions/MemberExpressionExtension.cs @@ -0,0 +1,51 @@ +using System.Linq.Expressions; + +namespace Carbunql.TypeSafe.Extensions; + +internal static class MemberExpressionExtension +{ + internal static string ToValue(this MemberExpression mem + , Func, string> mainConverter + , Func addParameter) + { + var tp = mem.Member.DeclaringType; + + if (tp == typeof(Sql)) + { + // ex. Sql.Now, Sql.CurrentTimestamp + return CreateSqlCommand(mem); + } + if (mem.Expression is MemberExpression && typeof(ITableRowDefinition).IsAssignableFrom(tp)) + { + //column + var table = ((MemberExpression)mem.Expression).Member.Name; + var column = mem.Member.Name; + return $"{table}.{column}"; + } + if (mem.Expression is ConstantExpression ce) + { + //variable + return addParameter(mem.CompileAndInvoke()); + } + + throw new NotSupportedException($"Member.Name:{mem.Member.Name}, Member.DeclaringType:{mem.Member.DeclaringType}"); + } + + private static string CreateSqlCommand(MemberExpression mem) + { + return mem.Member.Name switch + { + nameof(Sql.Now) => FluentSelectQuery.CreateCastStatement(DbmsConfiguration.GetNowCommandLogic(), typeof(DateTime)), + nameof(Sql.CurrentTimestamp) => DbmsConfiguration.GetCurrentTimestampCommandLogic(), + _ => throw new NotSupportedException($"The member '{mem.Member.Name}' is not supported.") + }; + } + + internal static object? CompileAndInvoke(this MemberExpression exp) + { + var delegateType = typeof(Func<>).MakeGenericType(exp.Type); + var lambda = Expression.Lambda(delegateType, exp); + var compiledLambda = lambda.Compile(); + return compiledLambda.DynamicInvoke(); + } +} diff --git a/src/Carbunql.TypeSafe/Extensions/MethodCallExpressionExtension.cs b/src/Carbunql.TypeSafe/Extensions/MethodCallExpressionExtension.cs index f2b4096f..d83ca971 100644 --- a/src/Carbunql.TypeSafe/Extensions/MethodCallExpressionExtension.cs +++ b/src/Carbunql.TypeSafe/Extensions/MethodCallExpressionExtension.cs @@ -38,7 +38,7 @@ internal static string ToValue(this MethodCallExpression mce return ToDateTimeValue(mce, mainConverter, addParameter); } - throw new NotSupportedException($"Type:{mce.Type}, DeclaringType:{mce.Method.DeclaringType}"); + throw new NotSupportedException($"Type:{mce.Type}, Method.DeclaringType:{mce.Method.DeclaringType}"); } private static string CreateMathCommand(this MethodCallExpression mce diff --git a/src/Carbunql.TypeSafe/FluentSelectQuery.cs b/src/Carbunql.TypeSafe/FluentSelectQuery.cs index e98af691..e3652620 100644 --- a/src/Carbunql.TypeSafe/FluentSelectQuery.cs +++ b/src/Carbunql.TypeSafe/FluentSelectQuery.cs @@ -141,26 +141,7 @@ private string ToValue(Expression exp, Func addParameter) if (exp is MemberExpression mem) { - var tp = mem.Member.DeclaringType; - - if (tp == typeof(Sql)) - { - // ex. Sql.Now, Sql.CurrentTimestamp - return CreateSqlCommand(mem); - } - if (mem.Expression is MemberExpression && typeof(ITableRowDefinition).IsAssignableFrom(tp)) - { - //column - var table = ((MemberExpression)mem.Expression).Member.Name; - var column = mem.Member.Name; - return $"{table}.{column}"; - } - if (mem.Expression is ConstantExpression ce) - { - //variable - return addParameter(mem.CompileAndInvoke()); - } - throw new InvalidProgramException(exp.ToString()); + return mem.ToValue(ToValue, addParameter); } else if (exp is ConstantExpression ce) { @@ -287,16 +268,6 @@ internal static string CreateCastStatement(string value, Type type) return $"cast({value} as {dbtype})"; } - private string CreateSqlCommand(MemberExpression mem) - { - return mem.Member.Name switch - { - nameof(Sql.Now) => CreateCastStatement(DbmsConfiguration.GetNowCommandLogic(), typeof(DateTime)), - nameof(Sql.CurrentTimestamp) => DbmsConfiguration.GetCurrentTimestampCommandLogic(), - _ => throw new NotSupportedException($"The member '{mem.Member.Name}' is not supported.") - }; - } - private string CreateCaseStatement(ConditionalExpression cnd, Func addParameter) { var test = ToValue(cnd.Test, addParameter); From 6a6236cd57e6fd082a8c55df49d45731ddebe2c0 Mon Sep 17 00:00:00 2001 From: mk3008 Date: Sun, 19 May 2024 14:54:08 +0900 Subject: [PATCH 17/23] Add Extension ConstantExpressionExtension NewExpressionExtension --- src/Carbunql.TypeSafe/ExpressionExtension.cs | 16 ------- .../Extensions/ConstantExpressionExtension.cs | 38 +++++++++++++++ .../Extensions/NewExpressionExtension.cs | 46 +++++++++++++++++++ src/Carbunql.TypeSafe/FluentSelectQuery.cs | 35 +------------- 4 files changed, 86 insertions(+), 49 deletions(-) delete mode 100644 src/Carbunql.TypeSafe/ExpressionExtension.cs create mode 100644 src/Carbunql.TypeSafe/Extensions/ConstantExpressionExtension.cs create mode 100644 src/Carbunql.TypeSafe/Extensions/NewExpressionExtension.cs diff --git a/src/Carbunql.TypeSafe/ExpressionExtension.cs b/src/Carbunql.TypeSafe/ExpressionExtension.cs deleted file mode 100644 index 2c863ed3..00000000 --- a/src/Carbunql.TypeSafe/ExpressionExtension.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System.Linq.Expressions; - -namespace Carbunql.TypeSafe; - -internal static class ExpressionExtension -{ - internal static object? CompileAndInvoke(this NewExpression exp) - { - var delegateType = typeof(Func<>).MakeGenericType(exp.Type); - var lambda = Expression.Lambda(delegateType, exp); - var compiledLambda = lambda.Compile(); - return compiledLambda.DynamicInvoke(); - } - - -} \ No newline at end of file diff --git a/src/Carbunql.TypeSafe/Extensions/ConstantExpressionExtension.cs b/src/Carbunql.TypeSafe/Extensions/ConstantExpressionExtension.cs new file mode 100644 index 00000000..5705cd0c --- /dev/null +++ b/src/Carbunql.TypeSafe/Extensions/ConstantExpressionExtension.cs @@ -0,0 +1,38 @@ +using System.Linq.Expressions; + +namespace Carbunql.TypeSafe.Extensions; + +internal static class ConstantExpressionExtension +{ + internal static string ToValue(this ConstantExpression ce + , Func, string> mainConverter + , Func addParameter) + { + var obj = ce.Value; + var tp = ce.Type; + + if (obj == null) + { + return "null"; + } + else if (tp == typeof(string)) + { + if (string.IsNullOrEmpty(obj.ToString())) + { + return "''"; + } + else + { + return addParameter(obj); + } + } + else if (tp == typeof(DateTime)) + { + return addParameter(obj); + } + else + { + return obj!.ToString()!; + } + } +} diff --git a/src/Carbunql.TypeSafe/Extensions/NewExpressionExtension.cs b/src/Carbunql.TypeSafe/Extensions/NewExpressionExtension.cs new file mode 100644 index 00000000..39f3f88e --- /dev/null +++ b/src/Carbunql.TypeSafe/Extensions/NewExpressionExtension.cs @@ -0,0 +1,46 @@ +using System.Linq.Expressions; + +namespace Carbunql.TypeSafe.Extensions; + +internal static class NewExpressionExtension +{ + internal static string ToValue(this NewExpression ne + , Func, string> mainConverter + , Func addParameter) + { + var obj = ne.CompileAndInvoke(); + var tp = ne.Type; + + if (obj == null) + { + return "null"; + } + else if (tp == typeof(string)) + { + if (string.IsNullOrEmpty(obj.ToString())) + { + return "''"; + } + else + { + return addParameter(obj); + } + } + else if (tp == typeof(DateTime)) + { + return addParameter(obj); + } + else + { + return obj!.ToString()!; + } + } + + internal static object? CompileAndInvoke(this NewExpression exp) + { + var delegateType = typeof(Func<>).MakeGenericType(exp.Type); + var lambda = Expression.Lambda(delegateType, exp); + var compiledLambda = lambda.Compile(); + return compiledLambda.DynamicInvoke(); + } +} diff --git a/src/Carbunql.TypeSafe/FluentSelectQuery.cs b/src/Carbunql.TypeSafe/FluentSelectQuery.cs index e3652620..a10e3e8b 100644 --- a/src/Carbunql.TypeSafe/FluentSelectQuery.cs +++ b/src/Carbunql.TypeSafe/FluentSelectQuery.cs @@ -110,48 +110,17 @@ public FluentSelectQuery Where(Expression> expression) private string ToValue(Expression exp, Func addParameter) { - Func fn = (obj, tp) => - { - if (obj == null) - { - return "null"; - } - else if (tp == typeof(string)) - { - if (string.IsNullOrEmpty(obj.ToString())) - { - return "''"; - } - else - { - return addParameter(obj); - } - } - else if (tp == typeof(DateTime)) - { - return addParameter(obj); - } - else - { - //var dbtype = DbmsConfiguration.ToDbType(tp); - //return $"cast({obj} as {dbtype})"; - return obj!.ToString()!; - } - }; - if (exp is MemberExpression mem) { return mem.ToValue(ToValue, addParameter); } else if (exp is ConstantExpression ce) { - // static value - return fn(ce.Value, ce.Type); + return ce.ToValue(ToValue, addParameter); } else if (exp is NewExpression ne) { - // ex. new Datetime - return fn(ne.CompileAndInvoke(), ne.Type); + return ne.ToValue(ToValue, addParameter); } else if (exp is BinaryExpression be) { From f0b8542a95f421625327a1899f9cb2d7d6d982c3 Mon Sep 17 00:00:00 2001 From: mk3008 Date: Sun, 19 May 2024 14:58:21 +0900 Subject: [PATCH 18/23] Add BinaryExpressionExtension --- .../Extensions/BinaryExpressionExtension.cs | 94 +++++++++++++++++++ src/Carbunql.TypeSafe/FluentSelectQuery.cs | 81 +--------------- 2 files changed, 95 insertions(+), 80 deletions(-) create mode 100644 src/Carbunql.TypeSafe/Extensions/BinaryExpressionExtension.cs diff --git a/src/Carbunql.TypeSafe/Extensions/BinaryExpressionExtension.cs b/src/Carbunql.TypeSafe/Extensions/BinaryExpressionExtension.cs new file mode 100644 index 00000000..6538d3a5 --- /dev/null +++ b/src/Carbunql.TypeSafe/Extensions/BinaryExpressionExtension.cs @@ -0,0 +1,94 @@ +using Carbunql.Analysis.Parser; +using System.Linq.Expressions; + +namespace Carbunql.TypeSafe.Extensions; + +internal static class BinaryExpressionExtension +{ + internal static string ToValue(this BinaryExpression be + , Func, string> mainConverter + , Func addParameter) + { + var left = mainConverter(be.Left, addParameter); + var right = mainConverter(be.Right, addParameter); + return ToValue(be.NodeType, left, right); + } + + private static string ToValue(ExpressionType nodeType, string left, string right) + { + var opPrecedence = GetPrecedenceFromExpressionType(nodeType); + + var leftValue = ValueParser.Parse(left); + var rightValue = ValueParser.Parse(right); + + // Enclose expressions in parentheses based on operator precedence or specific conditions + if (nodeType == ExpressionType.OrElse) + { + if (leftValue.GetOperators().Any()) + { + left = $"({left})"; + } + if (rightValue.GetOperators().Any()) + { + right = $"({right})"; + } + } + else if (opPrecedence == 2) + { + if (leftValue.GetOperators().Any(x => GetOperatorPrecedence(x) < opPrecedence)) + { + left = $"({left})"; + } + if (rightValue.GetOperators().Any(x => GetOperatorPrecedence(x) < opPrecedence)) + { + right = $"({right})"; + } + } + + // Return the formatted expression based on the operation type + return nodeType switch + { + ExpressionType.Coalesce => $"{DbmsConfiguration.CoalesceFunctionName}({left}, {right})", + ExpressionType.Add => $"{left} + {right}", + ExpressionType.Subtract => $"{left} - {right}", + ExpressionType.Multiply => $"{left} * {right}", + ExpressionType.Divide => $"{left} / {right}", + ExpressionType.Modulo => $"{DbmsConfiguration.GetModuloCommandLogic(left, right)}", + ExpressionType.Equal => $"{left} = {right}", + ExpressionType.NotEqual => $"{left} <> {right}", + ExpressionType.GreaterThan => $"{left} > {right}", + ExpressionType.GreaterThanOrEqual => $"{left} >= {right}", + ExpressionType.LessThan => $"{left} < {right}", + ExpressionType.LessThanOrEqual => $"{left} <= {right}", + ExpressionType.AndAlso => $"{left} and {right}", + ExpressionType.OrElse => $"{left} or {right}", + _ => throw new NotSupportedException($"Unsupported expression type: {nodeType}") + }; + } + + private static int GetPrecedenceFromExpressionType(ExpressionType nodeType) + { + var operatorText = nodeType switch + { + ExpressionType.Add => "+", + ExpressionType.Subtract => "-", + ExpressionType.Multiply => "*", + ExpressionType.Divide => "/", + _ => string.Empty, + }; + return GetOperatorPrecedence(operatorText); + } + + private static int GetOperatorPrecedence(string operatorText) + { + return operatorText switch + { + "+" => 1, + "-" => 1, + "*" => 2, + "/" => 2, + _ => 0, + }; + } + +} diff --git a/src/Carbunql.TypeSafe/FluentSelectQuery.cs b/src/Carbunql.TypeSafe/FluentSelectQuery.cs index a10e3e8b..200559ea 100644 --- a/src/Carbunql.TypeSafe/FluentSelectQuery.cs +++ b/src/Carbunql.TypeSafe/FluentSelectQuery.cs @@ -124,9 +124,7 @@ private string ToValue(Expression exp, Func addParameter) } else if (exp is BinaryExpression be) { - var left = ToValue(be.Left, addParameter); - var right = ToValue(be.Right, addParameter); - return ToValue(be.NodeType, left, right); + return be.ToValue(ToValue, addParameter); } else if (exp is UnaryExpression ue) { @@ -148,83 +146,6 @@ private string ToValue(Expression exp, Func addParameter) throw new InvalidProgramException(exp.ToString()); } - private string ToValue(ExpressionType nodeType, string left, string right) - { - var opPrecedence = GetPrecedenceFromExpressionType(nodeType); - - var leftValue = ValueParser.Parse(left); - var rightValue = ValueParser.Parse(right); - - // Enclose expressions in parentheses based on operator precedence or specific conditions - if (nodeType == ExpressionType.OrElse) - { - if (leftValue.GetOperators().Any()) - { - left = $"({left})"; - } - if (rightValue.GetOperators().Any()) - { - right = $"({right})"; - } - } - else if (opPrecedence == 2) - { - if (leftValue.GetOperators().Any(x => GetOperatorPrecedence(x) < opPrecedence)) - { - left = $"({left})"; - } - if (rightValue.GetOperators().Any(x => GetOperatorPrecedence(x) < opPrecedence)) - { - right = $"({right})"; - } - } - - // Return the formatted expression based on the operation type - return nodeType switch - { - ExpressionType.Coalesce => $"{DbmsConfiguration.CoalesceFunctionName}({left}, {right})", - ExpressionType.Add => $"{left} + {right}", - ExpressionType.Subtract => $"{left} - {right}", - ExpressionType.Multiply => $"{left} * {right}", - ExpressionType.Divide => $"{left} / {right}", - ExpressionType.Modulo => $"{DbmsConfiguration.GetModuloCommandLogic(left, right)}", - ExpressionType.Equal => $"{left} = {right}", - ExpressionType.NotEqual => $"{left} <> {right}", - ExpressionType.GreaterThan => $"{left} > {right}", - ExpressionType.GreaterThanOrEqual => $"{left} >= {right}", - ExpressionType.LessThan => $"{left} < {right}", - ExpressionType.LessThanOrEqual => $"{left} <= {right}", - ExpressionType.AndAlso => $"{left} and {right}", - ExpressionType.OrElse => $"{left} or {right}", - _ => throw new NotSupportedException($"Unsupported expression type: {nodeType}") - }; - } - - private static int GetOperatorPrecedence(string operatorText) - { - return operatorText switch - { - "+" => 1, - "-" => 1, - "*" => 2, - "/" => 2, - _ => 0, - }; - } - - private static int GetPrecedenceFromExpressionType(ExpressionType nodeType) - { - var operatorText = nodeType switch - { - ExpressionType.Add => "+", - ExpressionType.Subtract => "-", - ExpressionType.Multiply => "*", - ExpressionType.Divide => "/", - _ => string.Empty, - }; - return GetOperatorPrecedence(operatorText); - } - private string CreateCastStatement(UnaryExpression ue, Func addParameter) { var value = ToValue(ue.Operand, addParameter); From 5544b6b4a1f68099f6e44163769f43880398790b Mon Sep 17 00:00:00 2001 From: mk3008 Date: Sun, 19 May 2024 15:05:35 +0900 Subject: [PATCH 19/23] Add UnaryExpressionExtension --- .../Extensions/UnaryExpressionExtension.cs | 25 +++++++++++++++++++ src/Carbunql.TypeSafe/FluentSelectQuery.cs | 11 +------- 2 files changed, 26 insertions(+), 10 deletions(-) create mode 100644 src/Carbunql.TypeSafe/Extensions/UnaryExpressionExtension.cs diff --git a/src/Carbunql.TypeSafe/Extensions/UnaryExpressionExtension.cs b/src/Carbunql.TypeSafe/Extensions/UnaryExpressionExtension.cs new file mode 100644 index 00000000..872ff9d3 --- /dev/null +++ b/src/Carbunql.TypeSafe/Extensions/UnaryExpressionExtension.cs @@ -0,0 +1,25 @@ +using System.Linq.Expressions; + +namespace Carbunql.TypeSafe.Extensions; + +internal static class UnaryExpressionExtension +{ + internal static string ToValue(this UnaryExpression ue + , Func, string> mainConverter + , Func addParameter) + { + if (ue.NodeType == ExpressionType.Convert) + { + return ToConvertValue(ue, mainConverter, addParameter); + } + throw new InvalidProgramException($"NodeType:{ue.NodeType}"); + } + + private static string ToConvertValue(UnaryExpression ue + , Func, string> mainConverter + , Func addParameter) + { + var value = mainConverter(ue.Operand, addParameter); + return FluentSelectQuery.CreateCastStatement(value, ue.Type); + } +} diff --git a/src/Carbunql.TypeSafe/FluentSelectQuery.cs b/src/Carbunql.TypeSafe/FluentSelectQuery.cs index 200559ea..ed0adcb8 100644 --- a/src/Carbunql.TypeSafe/FluentSelectQuery.cs +++ b/src/Carbunql.TypeSafe/FluentSelectQuery.cs @@ -128,11 +128,7 @@ private string ToValue(Expression exp, Func addParameter) } else if (exp is UnaryExpression ue) { - if (ue.NodeType == ExpressionType.Convert) - { - return CreateCastStatement(ue, addParameter); - } - throw new InvalidProgramException(exp.ToString()); + return ue.ToValue(ToValue, addParameter); } else if (exp is MethodCallExpression mce) { @@ -146,11 +142,6 @@ private string ToValue(Expression exp, Func addParameter) throw new InvalidProgramException(exp.ToString()); } - private string CreateCastStatement(UnaryExpression ue, Func addParameter) - { - var value = ToValue(ue.Operand, addParameter); - return CreateCastStatement(value, ue.Type); - } internal static string CreateCastStatement(string value, Type type) { From 4cb25d7dbb4693e4489e6c00b53e6385d3062ea8 Mon Sep 17 00:00:00 2001 From: mk3008 Date: Sun, 19 May 2024 15:09:49 +0900 Subject: [PATCH 20/23] Add ConditionalExpressionExtension --- .../ConditionalExpressionExtension.cs | 42 +++++++++++++++++++ src/Carbunql.TypeSafe/FluentSelectQuery.cs | 36 +--------------- 2 files changed, 43 insertions(+), 35 deletions(-) create mode 100644 src/Carbunql.TypeSafe/Extensions/ConditionalExpressionExtension.cs diff --git a/src/Carbunql.TypeSafe/Extensions/ConditionalExpressionExtension.cs b/src/Carbunql.TypeSafe/Extensions/ConditionalExpressionExtension.cs new file mode 100644 index 00000000..7608dc21 --- /dev/null +++ b/src/Carbunql.TypeSafe/Extensions/ConditionalExpressionExtension.cs @@ -0,0 +1,42 @@ +using Carbunql.Analysis.Parser; +using System.Linq.Expressions; + +namespace Carbunql.TypeSafe.Extensions; + +internal static class ConditionalExpressionExtension +{ + internal static string ToValue(this ConditionalExpression cnd + , Func, string> mainConverter + , Func addParameter) + { + var test = mainConverter(cnd.Test, addParameter); + var ifTrue = mainConverter(cnd.IfTrue, addParameter); + var ifFalse = mainConverter(cnd.IfFalse, addParameter); + + if (string.IsNullOrEmpty(ifFalse)) + { + throw new ArgumentException("The IfFalse expression cannot be null or empty.", nameof(cnd.IfFalse)); + } + + // When case statements are nested, check if there is an alternative in the when clause + if (ifFalse.TrimStart().StartsWith("case ", StringComparison.OrdinalIgnoreCase)) + { + var caseExpression = CaseExpressionParser.Parse(ifFalse); + if (caseExpression.CaseCondition is null) + { + // Replace with when clause + var we = WhenExpressionParser.Parse($"when {test} then {ifTrue}"); + caseExpression.WhenExpressions.Insert(0, we); + return caseExpression.ToText(); + } + else + { + return $"case when {test} then {ifTrue} else {ifFalse} end"; + } + } + else + { + return $"case when {test} then {ifTrue} else {ifFalse} end"; + } + } +} diff --git a/src/Carbunql.TypeSafe/FluentSelectQuery.cs b/src/Carbunql.TypeSafe/FluentSelectQuery.cs index ed0adcb8..8faebcc5 100644 --- a/src/Carbunql.TypeSafe/FluentSelectQuery.cs +++ b/src/Carbunql.TypeSafe/FluentSelectQuery.cs @@ -136,52 +136,18 @@ private string ToValue(Expression exp, Func addParameter) } else if (exp is ConditionalExpression cnd) { - return CreateCaseStatement(cnd, addParameter); + return cnd.ToValue(ToValue, addParameter); } throw new InvalidProgramException(exp.ToString()); } - internal static string CreateCastStatement(string value, Type type) { var dbtype = DbmsConfiguration.ToDbType(type); return $"cast({value} as {dbtype})"; } - private string CreateCaseStatement(ConditionalExpression cnd, Func addParameter) - { - var test = ToValue(cnd.Test, addParameter); - var ifTrue = ToValue(cnd.IfTrue, addParameter); - var ifFalse = ToValue(cnd.IfFalse, addParameter); - - if (string.IsNullOrEmpty(ifFalse)) - { - throw new ArgumentException("The IfFalse expression cannot be null or empty.", nameof(cnd.IfFalse)); - } - - // When case statements are nested, check if there is an alternative in the when clause - if (ifFalse.TrimStart().StartsWith("case ", StringComparison.OrdinalIgnoreCase)) - { - var caseExpression = CaseExpressionParser.Parse(ifFalse); - if (caseExpression.CaseCondition is null) - { - // Replace with when clause - var we = WhenExpressionParser.Parse($"when {test} then {ifTrue}"); - caseExpression.WhenExpressions.Insert(0, we); - return caseExpression.ToText(); - } - else - { - return $"case when {test} then {ifTrue} else {ifFalse} end"; - } - } - else - { - return $"case when {test} then {ifTrue} else {ifFalse} end"; - } - } - internal static string ConverToDbDateFormat(string csharpFormat) { var replacements = new Dictionary From 3bcb22fe0ea33ea5aa4783f28c2f7bd54820bf4f Mon Sep 17 00:00:00 2001 From: mk3008 Date: Sun, 19 May 2024 15:14:14 +0900 Subject: [PATCH 21/23] Correcting redundant code --- .../MethodCallExpressionExtension.cs | 26 +++++++++- src/Carbunql.TypeSafe/FluentSelectQuery.cs | 47 +++---------------- 2 files changed, 32 insertions(+), 41 deletions(-) diff --git a/src/Carbunql.TypeSafe/Extensions/MethodCallExpressionExtension.cs b/src/Carbunql.TypeSafe/Extensions/MethodCallExpressionExtension.cs index d83ca971..a70a29bb 100644 --- a/src/Carbunql.TypeSafe/Extensions/MethodCallExpressionExtension.cs +++ b/src/Carbunql.TypeSafe/Extensions/MethodCallExpressionExtension.cs @@ -216,7 +216,7 @@ private static string ToStringValue(this MethodCallExpression mce { Func typeCaster = (obj) => { - var v = FluentSelectQuery.ConverToDbDateFormat(obj!.ToString()!); + var v = ConverToDbDateFormat(obj!.ToString()!); return addParameter(v); }; var typedArg = mainConverter(mce.Arguments[0], typeCaster); @@ -343,4 +343,28 @@ private static string RemoveRootBracketOrDefault(string value) } return value; } + + private static string ConverToDbDateFormat(string csharpFormat) + { + var replacements = new Dictionary + { + {"yyyy", "YYYY"}, + {"MM", "MM"}, + {"dd", "DD"}, + {"HH", "HH24"}, + {"mm", "MI"}, + {"ss", "SS"}, + {"ffffff", "US"}, + {"fff", "MS"} + }; + + string dbformat = csharpFormat; + + foreach (var pair in replacements) + { + dbformat = dbformat.Replace(pair.Key, pair.Value); + } + + return dbformat; + } } diff --git a/src/Carbunql.TypeSafe/FluentSelectQuery.cs b/src/Carbunql.TypeSafe/FluentSelectQuery.cs index 8faebcc5..70d6bff8 100644 --- a/src/Carbunql.TypeSafe/FluentSelectQuery.cs +++ b/src/Carbunql.TypeSafe/FluentSelectQuery.cs @@ -86,26 +86,17 @@ public FluentSelectQuery Where(Expression> expression) return pname; }; - if (expression.Body is MethodCallExpression mce) + var value = ToValue(expression.Body, addParameter); + + if (expression.Body is BinaryExpression be && be.NodeType == ExpressionType.OrElse) { - this.Where(ToValue(mce, addParameter)); - return this; + this.Where($"({value})"); } - else if (expression.Body is BinaryExpression be) + else { - var value = ToValue(be, addParameter); - if (be.NodeType == ExpressionType.OrElse) - { - this.Where($"({value})"); - } - else - { - this.Where(value); - } - return this; + this.Where(value); } - - throw new Exception(); + return this; } private string ToValue(Expression exp, Func addParameter) @@ -148,30 +139,6 @@ internal static string CreateCastStatement(string value, Type type) return $"cast({value} as {dbtype})"; } - internal static string ConverToDbDateFormat(string csharpFormat) - { - var replacements = new Dictionary - { - {"yyyy", "YYYY"}, - {"MM", "MM"}, - {"dd", "DD"}, - {"HH", "HH24"}, - {"mm", "MI"}, - {"ss", "SS"}, - {"ffffff", "US"}, - {"fff", "MS"} - }; - - string dbformat = csharpFormat; - - foreach (var pair in replacements) - { - dbformat = dbformat.Replace(pair.Key, pair.Value); - } - - return dbformat; - } - private string RemoveRootBracketOrDefault(string value) { if (value.StartsWith("(")) From 3a2dd3534372a76f17d2a12ee7981824fa8aa174 Mon Sep 17 00:00:00 2001 From: mk3008 Date: Sun, 19 May 2024 18:11:23 +0900 Subject: [PATCH 22/23] TypeSafe Query Builder Enhancements Supports IN clause. Supports ANY function. --- .../Extensions/MemberExpressionExtension.cs | 4 + .../MethodCallExpressionExtension.cs | 159 ++++++++++++++++-- .../ParameterExpressionExtension.cs | 17 ++ src/Carbunql.TypeSafe/FluentSelectQuery.cs | 4 + src/Carbunql/Values/OperatableValue.cs | 2 +- test/Carbunql.TypeSafe.Test/WhereTest.cs | 75 +++++++++ 6 files changed, 249 insertions(+), 12 deletions(-) create mode 100644 src/Carbunql.TypeSafe/Extensions/ParameterExpressionExtension.cs diff --git a/src/Carbunql.TypeSafe/Extensions/MemberExpressionExtension.cs b/src/Carbunql.TypeSafe/Extensions/MemberExpressionExtension.cs index 52e057b7..6635e992 100644 --- a/src/Carbunql.TypeSafe/Extensions/MemberExpressionExtension.cs +++ b/src/Carbunql.TypeSafe/Extensions/MemberExpressionExtension.cs @@ -27,6 +27,10 @@ internal static string ToValue(this MemberExpression mem //variable return addParameter(mem.CompileAndInvoke()); } + if (mem.Expression is MemberExpression me) + { + return me.ToValue(mainConverter, addParameter); + } throw new NotSupportedException($"Member.Name:{mem.Member.Name}, Member.DeclaringType:{mem.Member.DeclaringType}"); } diff --git a/src/Carbunql.TypeSafe/Extensions/MethodCallExpressionExtension.cs b/src/Carbunql.TypeSafe/Extensions/MethodCallExpressionExtension.cs index a70a29bb..b34ada98 100644 --- a/src/Carbunql.TypeSafe/Extensions/MethodCallExpressionExtension.cs +++ b/src/Carbunql.TypeSafe/Extensions/MethodCallExpressionExtension.cs @@ -1,5 +1,6 @@ using Carbunql.Analysis.Parser; using Carbunql.Values; +using System.Collections; using System.Linq.Expressions; namespace Carbunql.TypeSafe.Extensions; @@ -250,29 +251,114 @@ private static string ToBoolValue(this MethodCallExpression mce { if (mce.Object != null) { - var value = mainConverter(mce.Object, addParameter); - if (mce.Arguments.Count == 1) { - var arg = mainConverter(mce.Arguments[0], addParameter); - if (mce.Method.Name == nameof(String.StartsWith)) + if (mce.Method.DeclaringType == typeof(string)) { - return $"{value} like {arg} || '%'"; + var value = mainConverter(mce.Object, addParameter); + + var arg = mainConverter(mce.Arguments[0], addParameter); + if (mce.Method.Name == nameof(String.StartsWith)) + { + return $"{value} like {arg} || '%'"; + } + if (mce.Method.Name == nameof(String.Contains)) + { + return $"{value} like '%' || {arg} || '%'"; + } + if (mce.Method.Name == nameof(String.EndsWith)) + { + return $"{value} like {arg} || '%'"; + } } - if (mce.Method.Name == nameof(String.Contains)) + if (IsGenericList(mce.Object.Type)) { - return $"{value} like '%' || {arg} || '%'"; + //The IN clause itself is not suitable for parameter queries, so the collection will be forcibly expanded. + //If you want to parameterize it, use the ANY function, etc. + var args = new List(); + Func argumentsDecoder = collection => + { + if (collection != null && IsGenericList(collection.GetType())) + { + foreach (var item in (IEnumerable)collection) + { + args.Add(AddParameter(item, addParameter)); + } + } + return string.Empty; + }; + + _ = mainConverter(mce.Object, argumentsDecoder); + + var left = mainConverter(mce.Arguments[0], addParameter); + if (mce.Method.Name == nameof(String.Contains)) + { + return $"{left} in({string.Join(",", args)})"; + } } - if (mce.Method.Name == nameof(String.EndsWith)) + } + + throw new NotSupportedException($"Object:{mce.Object.Type.FullName}, Method.Type:{mce.Method.DeclaringType}, Method.Method:{mce.Method.Name}, Arguments:{mce.Arguments.Count}, Type:{mce.Type}"); + } + else + { + if (mce.Arguments.Count == 2) + { + if (mce.Method.DeclaringType == typeof(Enumerable)) { - return $"{value} like {arg} || '%'"; + if (mce.Method.Name == nameof(Enumerable.Any)) + { + return ToAnyClauseValue(mce, mainConverter, addParameter); + } } } + throw new NotSupportedException($"Object:NULL, Method.Type:{mce.Method.DeclaringType}, Method.Name:{mce.Method.Name}, Arguments:{mce.Arguments.Count}, Type:{mce.Type}"); + } + } - throw new NotSupportedException($"Object:{mce.Object.Type.FullName}, Method:{mce.Method.Name}, Arguments:{mce.Arguments.Count}, Type:{mce.Type}"); + private static string ToAnyClauseValue(this MethodCallExpression mce + , Func, string> mainConverter + , Func addParameter) + { + // Format: + // Array.Any(x => (table.column == x)) + // or + // Array.Any(x => (x == table.column)) + + var lambda = (LambdaExpression)mce.Arguments[1]; + var variableName = lambda.Parameters[0].Name!; + + var arrayParameterName = mainConverter(mce.Arguments[0], addParameter); + + // Hook the parameter name and return in the format any(PARAMETER) + var hasAnyCommand = false; + Func interceptor = x => + { + if (variableName.Equals(x)) + { + hasAnyCommand = true; + return $"any({arrayParameterName})"; + } + throw new InvalidProgramException(); + }; + + var body = (BinaryExpression)lambda.Body; + if (body.NodeType != ExpressionType.Equal) throw new InvalidProgramException(); + + // Adjust to make the ANY function appear on the right side + var left = mainConverter(body.Left, interceptor); + if (hasAnyCommand) + { + return $"{mainConverter(body.Right, interceptor)} = {left}"; } - throw new NotSupportedException($"Object:NULL, Method:{mce.Method.Name}, Arguments:{mce.Arguments.Count}, Type:{mce.Type}"); + var right = mainConverter(body.Right, interceptor); + if (hasAnyCommand) + { + return $"{left} = {right}"; + } + + throw new InvalidProgramException(); } private static string ToDateTimeValue(this MethodCallExpression mce @@ -367,4 +453,55 @@ private static string ConverToDbDateFormat(string csharpFormat) return dbformat; } + + private static bool IsGenericList(Type? type) + { + if (type == null) return false; + + if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(IList<>)) + { + return true; + } + + foreach (Type intf in type.GetInterfaces()) + { + if (intf.IsGenericType && intf.GetGenericTypeDefinition() == typeof(IList<>)) + { + return true; + } + } + + return false; + } + + private static string AddParameter(Object? value + , Func addParameter) + { + if (value == null) + { + return "null"; + } + + var tp = value.GetType(); + + if (tp == typeof(string)) + { + if (string.IsNullOrEmpty(value.ToString())) + { + return "''"; + } + else + { + return addParameter(value); + } + } + else if (tp == typeof(DateTime)) + { + return addParameter(value); + } + else + { + return value.ToString()!; + } + } } diff --git a/src/Carbunql.TypeSafe/Extensions/ParameterExpressionExtension.cs b/src/Carbunql.TypeSafe/Extensions/ParameterExpressionExtension.cs new file mode 100644 index 00000000..2ff2b950 --- /dev/null +++ b/src/Carbunql.TypeSafe/Extensions/ParameterExpressionExtension.cs @@ -0,0 +1,17 @@ +using System.Linq.Expressions; + +namespace Carbunql.TypeSafe.Extensions; + +internal static class ParameterExpressionExtension +{ + internal static string ToValue(this ParameterExpression prm + , Func, string> mainConverter + , Func addParameter) + { + if (string.IsNullOrEmpty(prm.Name)) + { + throw new Exception(); + } + return addParameter(prm.Name); + } +} diff --git a/src/Carbunql.TypeSafe/FluentSelectQuery.cs b/src/Carbunql.TypeSafe/FluentSelectQuery.cs index 70d6bff8..bc17c360 100644 --- a/src/Carbunql.TypeSafe/FluentSelectQuery.cs +++ b/src/Carbunql.TypeSafe/FluentSelectQuery.cs @@ -129,6 +129,10 @@ private string ToValue(Expression exp, Func addParameter) { return cnd.ToValue(ToValue, addParameter); } + else if (exp is ParameterExpression prm) + { + return prm.ToValue(ToValue, addParameter); + } throw new InvalidProgramException(exp.ToString()); } diff --git a/src/Carbunql/Values/OperatableValue.cs b/src/Carbunql/Values/OperatableValue.cs index bc2a5575..3114a1d5 100644 --- a/src/Carbunql/Values/OperatableValue.cs +++ b/src/Carbunql/Values/OperatableValue.cs @@ -24,7 +24,7 @@ public OperatableValue(string @operator, ValueBase value) /// /// Gets or sets the operator to apply to the value. /// - public string Operator { get; init; } + public string Operator { get; init ; } /// /// Gets or sets the value to which the operator is applied. diff --git a/test/Carbunql.TypeSafe.Test/WhereTest.cs b/test/Carbunql.TypeSafe.Test/WhereTest.cs index bbaa180b..1136a2fd 100644 --- a/test/Carbunql.TypeSafe.Test/WhereTest.cs +++ b/test/Carbunql.TypeSafe.Test/WhereTest.cs @@ -178,6 +178,81 @@ sale AS a Assert.Equal(expect, actual, true, true, true); } + [Fact] + public void InTest() + { + var idArray = new List() { 1, 2, 3, 4 }; + + var a = Sql.DefineTable(); + + var query = Sql.From(() => a) + .Where(() => idArray.Contains(a.sale_id!.Value)); + + var actual = query.ToText(); + Output.WriteLine(query.ToText()); + + var expect = @"SELECT + * +FROM + sale AS a +WHERE + a.sale_id IN (1, 2, 3, 4)"; + + Assert.Equal(expect, actual, true, true, true); + } + + [Fact] + public void AnyTest() + { + var idArray = new List() { 1, 2, 3, 4 }; + + var a = Sql.DefineTable(); + + var query = Sql.From(() => a) + .Where(() => idArray.Any(x => a.sale_id!.Value == x)); + + var actual = query.ToText(); + Output.WriteLine(query.ToText()); + + var expect = @"/* + :p0 = System.Collections.Generic.List`1[System.Int32] +*/ +SELECT + * +FROM + sale AS a +WHERE + a.sale_id = ANY(:p0)"; + + Assert.Equal(expect, actual, true, true, true); + } + + [Fact] + public void AnyTest_Right() + { + var idArray = new List() { 1, 2, 3, 4 }; + + var a = Sql.DefineTable(); + + var query = Sql.From(() => a) + .Where(() => idArray.Any(x => x == a.sale_id!.Value)); + + var actual = query.ToText(); + Output.WriteLine(query.ToText()); + + var expect = @"/* + :p0 = System.Collections.Generic.List`1[System.Int32] +*/ +SELECT + * +FROM + sale AS a +WHERE + a.sale_id = ANY(:p0)"; + + Assert.Equal(expect, actual, true, true, true); + } + public record sale( int? sale_id, string product_name, From 4443070b11abfd6999bc9a5ced1891a937ea7afc Mon Sep 17 00:00:00 2001 From: mk3008 Date: Tue, 21 May 2024 21:31:40 +0900 Subject: [PATCH 23/23] Improved TypeSafe build function Changed to get parameter name from variable name. --- .../Carbunql.TypeSafe.csproj | 17 ++-- .../Extensions/BinaryExpressionExtension.cs | 4 +- .../ConditionalExpressionExtension.cs | 4 +- .../Extensions/ConstantExpressionExtension.cs | 8 +- .../Extensions/MemberExpressionExtension.cs | 6 +- .../MethodCallExpressionExtension.cs | 50 +++++------ .../Extensions/NewExpressionExtension.cs | 8 +- .../ParameterExpressionExtension.cs | 6 +- .../Extensions/UnaryExpressionExtension.cs | 8 +- src/Carbunql.TypeSafe/FluentSelectQuery.cs | 56 ++---------- src/Carbunql.TypeSafe/ParameterManager.cs | 89 +++++++++++++++++++ .../Carbunql.TypeSafe.Test/SingleTableTest.cs | 56 ++++++------ test/Carbunql.TypeSafe.Test/WhereTest.cs | 12 +-- 13 files changed, 184 insertions(+), 140 deletions(-) create mode 100644 src/Carbunql.TypeSafe/ParameterManager.cs diff --git a/src/Carbunql.TypeSafe/Carbunql.TypeSafe.csproj b/src/Carbunql.TypeSafe/Carbunql.TypeSafe.csproj index 97f4473a..e3fa734a 100644 --- a/src/Carbunql.TypeSafe/Carbunql.TypeSafe.csproj +++ b/src/Carbunql.TypeSafe/Carbunql.TypeSafe.csproj @@ -1,13 +1,14 @@ - - net6.0 - enable - enable - + + net6.0 + enable + enable + 12.0 + - - - + + + diff --git a/src/Carbunql.TypeSafe/Extensions/BinaryExpressionExtension.cs b/src/Carbunql.TypeSafe/Extensions/BinaryExpressionExtension.cs index 6538d3a5..ed7fb9cf 100644 --- a/src/Carbunql.TypeSafe/Extensions/BinaryExpressionExtension.cs +++ b/src/Carbunql.TypeSafe/Extensions/BinaryExpressionExtension.cs @@ -6,8 +6,8 @@ namespace Carbunql.TypeSafe.Extensions; internal static class BinaryExpressionExtension { internal static string ToValue(this BinaryExpression be - , Func, string> mainConverter - , Func addParameter) + , Func, string> mainConverter + , Func addParameter) { var left = mainConverter(be.Left, addParameter); var right = mainConverter(be.Right, addParameter); diff --git a/src/Carbunql.TypeSafe/Extensions/ConditionalExpressionExtension.cs b/src/Carbunql.TypeSafe/Extensions/ConditionalExpressionExtension.cs index 7608dc21..f9dd6e66 100644 --- a/src/Carbunql.TypeSafe/Extensions/ConditionalExpressionExtension.cs +++ b/src/Carbunql.TypeSafe/Extensions/ConditionalExpressionExtension.cs @@ -6,8 +6,8 @@ namespace Carbunql.TypeSafe.Extensions; internal static class ConditionalExpressionExtension { internal static string ToValue(this ConditionalExpression cnd - , Func, string> mainConverter - , Func addParameter) + , Func, string> mainConverter + , Func addParameter) { var test = mainConverter(cnd.Test, addParameter); var ifTrue = mainConverter(cnd.IfTrue, addParameter); diff --git a/src/Carbunql.TypeSafe/Extensions/ConstantExpressionExtension.cs b/src/Carbunql.TypeSafe/Extensions/ConstantExpressionExtension.cs index 5705cd0c..5c03fbb7 100644 --- a/src/Carbunql.TypeSafe/Extensions/ConstantExpressionExtension.cs +++ b/src/Carbunql.TypeSafe/Extensions/ConstantExpressionExtension.cs @@ -5,8 +5,8 @@ namespace Carbunql.TypeSafe.Extensions; internal static class ConstantExpressionExtension { internal static string ToValue(this ConstantExpression ce - , Func, string> mainConverter - , Func addParameter) + , Func, string> mainConverter + , Func addParameter) { var obj = ce.Value; var tp = ce.Type; @@ -23,12 +23,12 @@ internal static string ToValue(this ConstantExpression ce } else { - return addParameter(obj); + return addParameter(string.Empty, obj); } } else if (tp == typeof(DateTime)) { - return addParameter(obj); + return addParameter(string.Empty, obj); } else { diff --git a/src/Carbunql.TypeSafe/Extensions/MemberExpressionExtension.cs b/src/Carbunql.TypeSafe/Extensions/MemberExpressionExtension.cs index 6635e992..62f0e5f7 100644 --- a/src/Carbunql.TypeSafe/Extensions/MemberExpressionExtension.cs +++ b/src/Carbunql.TypeSafe/Extensions/MemberExpressionExtension.cs @@ -5,8 +5,8 @@ namespace Carbunql.TypeSafe.Extensions; internal static class MemberExpressionExtension { internal static string ToValue(this MemberExpression mem - , Func, string> mainConverter - , Func addParameter) + , Func, string> mainConverter + , Func addParameter) { var tp = mem.Member.DeclaringType; @@ -25,7 +25,7 @@ internal static string ToValue(this MemberExpression mem if (mem.Expression is ConstantExpression ce) { //variable - return addParameter(mem.CompileAndInvoke()); + return addParameter(mem.Member.Name, mem.CompileAndInvoke()); } if (mem.Expression is MemberExpression me) { diff --git a/src/Carbunql.TypeSafe/Extensions/MethodCallExpressionExtension.cs b/src/Carbunql.TypeSafe/Extensions/MethodCallExpressionExtension.cs index b34ada98..a124ee77 100644 --- a/src/Carbunql.TypeSafe/Extensions/MethodCallExpressionExtension.cs +++ b/src/Carbunql.TypeSafe/Extensions/MethodCallExpressionExtension.cs @@ -8,8 +8,8 @@ namespace Carbunql.TypeSafe.Extensions; internal static class MethodCallExpressionExtension { internal static string ToValue(this MethodCallExpression mce - , Func, string> mainConverter - , Func addParameter) + , Func, string> mainConverter + , Func addParameter) { // DeclaringType if (mce.Method.DeclaringType == typeof(Math)) @@ -43,8 +43,8 @@ internal static string ToValue(this MethodCallExpression mce } private static string CreateMathCommand(this MethodCallExpression mce - , Func, string> mainConverter - , Func addParameter) + , Func, string> mainConverter + , Func addParameter) { var args = mce.Arguments.Select(x => RemoveRootBracketOrDefault(mainConverter(x, addParameter))); @@ -59,8 +59,8 @@ private static string CreateMathCommand(this MethodCallExpression mce } private static string CreateDateTimeExtensionCommand(MethodCallExpression mce - , Func, string> mainConverter - , Func addParameter) + , Func, string> mainConverter + , Func addParameter) { switch (mce.Method.Name) { @@ -96,8 +96,8 @@ private static string CreateDateTimeExtensionCommand(MethodCallExpression mce } private static string CreateSqlCommand(this MethodCallExpression mce - , Func, string> mainConverter - , Func addParameter) + , Func, string> mainConverter + , Func addParameter) { switch (mce.Method.Name) { @@ -185,8 +185,8 @@ private static string CreateSqlCommand(this MethodCallExpression mce } private static string ToStringValue(this MethodCallExpression mce - , Func, string> mainConverter - , Func addParameter) + , Func, string> mainConverter + , Func addParameter) { if (mce.Object != null) { @@ -215,10 +215,10 @@ private static string ToStringValue(this MethodCallExpression mce { if (mce.Method.Name == nameof(String.ToString)) { - Func typeCaster = (obj) => + Func typeCaster = (key, value) => { - var v = ConverToDbDateFormat(obj!.ToString()!); - return addParameter(v); + var v = ConverToDbDateFormat(value!.ToString()!); + return addParameter(key, v); }; var typedArg = mainConverter(mce.Arguments[0], typeCaster); return $"to_char({value}, {typedArg})"; @@ -246,8 +246,8 @@ private static string ToStringValue(this MethodCallExpression mce } private static string ToBoolValue(this MethodCallExpression mce - , Func, string> mainConverter - , Func addParameter) + , Func, string> mainConverter + , Func addParameter) { if (mce.Object != null) { @@ -276,7 +276,7 @@ private static string ToBoolValue(this MethodCallExpression mce //The IN clause itself is not suitable for parameter queries, so the collection will be forcibly expanded. //If you want to parameterize it, use the ANY function, etc. var args = new List(); - Func argumentsDecoder = collection => + Func argumentsDecoder = (_, collection) => { if (collection != null && IsGenericList(collection.GetType())) { @@ -317,8 +317,8 @@ private static string ToBoolValue(this MethodCallExpression mce } private static string ToAnyClauseValue(this MethodCallExpression mce - , Func, string> mainConverter - , Func addParameter) + , Func, string> mainConverter + , Func addParameter) { // Format: // Array.Any(x => (table.column == x)) @@ -332,7 +332,7 @@ private static string ToAnyClauseValue(this MethodCallExpression mce // Hook the parameter name and return in the format any(PARAMETER) var hasAnyCommand = false; - Func interceptor = x => + Func interceptor = (_, x) => { if (variableName.Equals(x)) { @@ -362,8 +362,8 @@ private static string ToAnyClauseValue(this MethodCallExpression mce } private static string ToDateTimeValue(this MethodCallExpression mce - , Func, string> mainConverter - , Func addParameter) + , Func, string> mainConverter + , Func addParameter) { if (mce!.Object != null) { @@ -409,7 +409,7 @@ private static string ToDateTimeValue(this MethodCallExpression mce { if (mce.Arguments.Count == 1) { - Func echo = x => x!.ToString()!; + Func echo = (_, x) => x!.ToString()!; return mce.ToValue(mainConverter, echo); } @@ -475,7 +475,7 @@ private static bool IsGenericList(Type? type) } private static string AddParameter(Object? value - , Func addParameter) + , Func addParameter) { if (value == null) { @@ -492,12 +492,12 @@ private static string AddParameter(Object? value } else { - return addParameter(value); + return addParameter(string.Empty, value); } } else if (tp == typeof(DateTime)) { - return addParameter(value); + return addParameter(string.Empty, value); } else { diff --git a/src/Carbunql.TypeSafe/Extensions/NewExpressionExtension.cs b/src/Carbunql.TypeSafe/Extensions/NewExpressionExtension.cs index 39f3f88e..10f56695 100644 --- a/src/Carbunql.TypeSafe/Extensions/NewExpressionExtension.cs +++ b/src/Carbunql.TypeSafe/Extensions/NewExpressionExtension.cs @@ -5,8 +5,8 @@ namespace Carbunql.TypeSafe.Extensions; internal static class NewExpressionExtension { internal static string ToValue(this NewExpression ne - , Func, string> mainConverter - , Func addParameter) + , Func, string> mainConverter + , Func addParameter) { var obj = ne.CompileAndInvoke(); var tp = ne.Type; @@ -23,12 +23,12 @@ internal static string ToValue(this NewExpression ne } else { - return addParameter(obj); + return addParameter(string.Empty, obj); } } else if (tp == typeof(DateTime)) { - return addParameter(obj); + return addParameter(string.Empty, obj); } else { diff --git a/src/Carbunql.TypeSafe/Extensions/ParameterExpressionExtension.cs b/src/Carbunql.TypeSafe/Extensions/ParameterExpressionExtension.cs index 2ff2b950..9436b773 100644 --- a/src/Carbunql.TypeSafe/Extensions/ParameterExpressionExtension.cs +++ b/src/Carbunql.TypeSafe/Extensions/ParameterExpressionExtension.cs @@ -5,13 +5,13 @@ namespace Carbunql.TypeSafe.Extensions; internal static class ParameterExpressionExtension { internal static string ToValue(this ParameterExpression prm - , Func, string> mainConverter - , Func addParameter) + , Func, string> mainConverter + , Func addParameter) { if (string.IsNullOrEmpty(prm.Name)) { throw new Exception(); } - return addParameter(prm.Name); + return addParameter(string.Empty, prm.Name); } } diff --git a/src/Carbunql.TypeSafe/Extensions/UnaryExpressionExtension.cs b/src/Carbunql.TypeSafe/Extensions/UnaryExpressionExtension.cs index 872ff9d3..fb8c8e30 100644 --- a/src/Carbunql.TypeSafe/Extensions/UnaryExpressionExtension.cs +++ b/src/Carbunql.TypeSafe/Extensions/UnaryExpressionExtension.cs @@ -5,8 +5,8 @@ namespace Carbunql.TypeSafe.Extensions; internal static class UnaryExpressionExtension { internal static string ToValue(this UnaryExpression ue - , Func, string> mainConverter - , Func addParameter) + , Func, string> mainConverter + , Func addParameter) { if (ue.NodeType == ExpressionType.Convert) { @@ -16,8 +16,8 @@ internal static string ToValue(this UnaryExpression ue } private static string ToConvertValue(UnaryExpression ue - , Func, string> mainConverter - , Func addParameter) + , Func, string> mainConverter + , Func addParameter) { var value = mainConverter(ue.Operand, addParameter); return FluentSelectQuery.CreateCastStatement(value, ue.Type); diff --git a/src/Carbunql.TypeSafe/FluentSelectQuery.cs b/src/Carbunql.TypeSafe/FluentSelectQuery.cs index bc17c360..c3d3d1e4 100644 --- a/src/Carbunql.TypeSafe/FluentSelectQuery.cs +++ b/src/Carbunql.TypeSafe/FluentSelectQuery.cs @@ -16,30 +16,7 @@ public FluentSelectQuery Select(Expression> expression) where T : cla var body = (NewExpression)expression.Body; - var prms = this.GetParameters().ToList(); - var parameterCount = prms.Count(); - Func addParameter = (obj) => - { - if (obj != null) - { - var q = prms.Where(x => x.Value != null && x.Value.Equals(obj)); - if (q.Any()) - { - return q.First().ParameterName; - } - } - - var pname = $"{DbmsConfiguration.PlaceholderIdentifier}p{parameterCount}"; - parameterCount++; - AddParameter(pname, obj); - - if (obj != null) - { - prms.Add(new QueryParameter(pname, obj)); - } - - return pname; - }; + var prmManager = new ParameterManager(GetParameters(), AddParameter); if (body.Members != null) { @@ -47,7 +24,7 @@ public FluentSelectQuery Select(Expression> expression) where T : cla for (var i = 0; i < cnt; i++) { var alias = body.Members[i].Name; - var value = ToValue(body.Arguments[i], addParameter); + var value = ToValue(body.Arguments[i], prmManager.AddParaemter); this.Select(RemoveRootBracketOrDefault(value)).As(alias); } } @@ -61,32 +38,9 @@ public FluentSelectQuery Where(Expression> expression) var analyzed = ExpressionReader.Analyze(expression); #endif - var prms = this.GetParameters().ToList(); - var parameterCount = prms.Count(); - Func addParameter = (obj) => - { - if (obj != null) - { - var q = prms.Where(x => x.Value != null && x.Value.Equals(obj)); - if (q.Any()) - { - return q.First().ParameterName; - } - } - - var pname = $"{DbmsConfiguration.PlaceholderIdentifier}p{parameterCount}"; - parameterCount++; - AddParameter(pname, obj); - - if (obj != null) - { - prms.Add(new QueryParameter(pname, obj)); - } - - return pname; - }; + var prmManager = new ParameterManager(GetParameters(), AddParameter); - var value = ToValue(expression.Body, addParameter); + var value = ToValue(expression.Body, prmManager.AddParaemter); if (expression.Body is BinaryExpression be && be.NodeType == ExpressionType.OrElse) { @@ -99,7 +53,7 @@ public FluentSelectQuery Where(Expression> expression) return this; } - private string ToValue(Expression exp, Func addParameter) + private string ToValue(Expression exp, Func addParameter) { if (exp is MemberExpression mem) { diff --git a/src/Carbunql.TypeSafe/ParameterManager.cs b/src/Carbunql.TypeSafe/ParameterManager.cs new file mode 100644 index 00000000..7da612d8 --- /dev/null +++ b/src/Carbunql.TypeSafe/ParameterManager.cs @@ -0,0 +1,89 @@ +using Carbunql.Building; + +namespace Carbunql.TypeSafe; + +internal class ParameterManager +{ + public ParameterManager(IEnumerable parameters, Func addParameterLogic) + { + Parameters = parameters.ToDictionary(x => x.ParameterName, x => x.Value); + AddParameterLogic = addParameterLogic; + } + + public Dictionary Parameters { get; set; } + + public Func AddParameterLogic { get; set; } + + /// + /// Generates an available parameter name. + /// The returned parameter name may not necessarily be unused. + /// Even if a parameter name is already used, it can be reused if the value matches as well. + /// + /// Variable name + /// Variable value + /// Returns an available parameter name. It also indicates whether the parameter name is being reused. + private Result GenerateParameterName(string variableName, object? value) + { + // If there is no variable name, and there is exactly one match with the same value, reuse it + if (string.IsNullOrEmpty(variableName)) + { + var q = Parameters.Where(x => x.Value != null && x.Value.Equals(value)); + if (q.Count() == 1) + { + var kv = q.First(); + return new Result(kv.Key, true); + } + } + + // Temporarily set the parameter name based on the variable name + var requestKey = !string.IsNullOrEmpty(variableName) ? $"{DbmsConfiguration.PlaceholderIdentifier}{variableName.ToLowerSnakeCase()}" : $"{DbmsConfiguration.PlaceholderIdentifier}p{Parameters.Count}"; + + // If the temporary parameter is unused, the desired parameter can be used as is + if (!Parameters.ContainsKey(requestKey)) + { + return new Result(requestKey, false); + } + + var existingValue = Parameters[requestKey]; + + // Even if used, if the existing parameter and key match the value, it can be reused + if (existingValue != null && existingValue.Equals(value)) + { + return new Result(requestKey, true); + } + + // Otherwise, automatically assign a name. + var index = 0; + string tmpkey; + do + { + tmpkey = $"{requestKey}_{index++}"; + } while (Parameters.ContainsKey(tmpkey)); + + return new Result(tmpkey, false); + } + + public string AddParaemter(string key, object? value) + { + var nameInfo = GenerateParameterName(key, value); + + if (nameInfo.IsReuse) return nameInfo.ParameterName; + + AddParameterLogic(nameInfo.ParameterName, value); + Parameters.Add(nameInfo.ParameterName, value); + + return nameInfo.ParameterName; + } + + internal struct Result + { + public string ParameterName { get; } + public bool IsReuse { get; } + + public Result(string parameterName, bool isReuse) + { + ParameterName = parameterName; + IsReuse = isReuse; + } + } +} \ No newline at end of file diff --git a/test/Carbunql.TypeSafe.Test/SingleTableTest.cs b/test/Carbunql.TypeSafe.Test/SingleTableTest.cs index 013872cd..50598a2d 100644 --- a/test/Carbunql.TypeSafe.Test/SingleTableTest.cs +++ b/test/Carbunql.TypeSafe.Test/SingleTableTest.cs @@ -166,20 +166,20 @@ public void VariableTest() Output.WriteLine(actual); var expect = @"/* - :p0 = 1 - :p1 = 10 - :p2 = 0.1 - :p3 = True - :p4 = 'test' - :p5 = 2000/01/01 10:10:00 + :id = 1 + :value = 10 + :rate = 0.1 + :tf_value = True + :remarks = 'test' + :created_at = 2000/01/01 10:10:00 */ SELECT - :p0 AS id, - :p1 AS value, - :p2 AS rate, - :p3 AS tf_value, - :p4 AS remarks, - :p5 AS created_at + :id AS id, + :value AS value, + :rate AS rate, + :tf_value AS tf_value, + :remarks AS remarks, + :created_at AS created_at FROM sale AS a"; @@ -270,16 +270,16 @@ public void DatetimeTest_SqlCommand() Output.WriteLine(actual); var expect = @"/* - :p0 = 2000/10/20 0:00:00 + :d = 2000/10/20 0:00:00 */ SELECT - DATE_TRUNC('year', :p0) AS v_trunc_year, - DATE_TRUNC('quarter', :p0) AS v_trunc_quarter, - DATE_TRUNC('month', :p0) AS v_trunc_month, - DATE_TRUNC('day', :p0) AS v_trunc_day, - DATE_TRUNC('hour', :p0) AS v_trunc_hour, - DATE_TRUNC('minute', :p0) AS v_trunc_minute, - DATE_TRUNC('second', :p0) AS v_trunc_second, + DATE_TRUNC('year', :d) AS v_trunc_year, + DATE_TRUNC('quarter', :d) AS v_trunc_quarter, + DATE_TRUNC('month', :d) AS v_trunc_month, + DATE_TRUNC('day', :d) AS v_trunc_day, + DATE_TRUNC('hour', :d) AS v_trunc_hour, + DATE_TRUNC('minute', :d) AS v_trunc_minute, + DATE_TRUNC('second', :d) AS v_trunc_second, DATE_TRUNC('month', CAST(NOW() AS timestamp)) + 1 * INTERVAL '1 month' + -1 * INTERVAL '1 day' AS last_date_of_month FROM sale AS a"; @@ -312,16 +312,16 @@ public void DatetimeTest_SqlExtension() Output.WriteLine(actual); var expect = @"/* - :p0 = 2000/10/20 0:00:00 + :d = 2000/10/20 0:00:00 */ SELECT - DATE_TRUNC('year', :p0) AS v_trunc_year, - DATE_TRUNC('quarter', :p0) AS v_trunc_quarter, - DATE_TRUNC('month', :p0) AS v_trunc_month, - DATE_TRUNC('day', :p0) AS v_trunc_day, - DATE_TRUNC('hour', :p0) AS v_trunc_hour, - DATE_TRUNC('minute', :p0) AS v_trunc_minute, - DATE_TRUNC('second', :p0) AS v_trunc_second, + DATE_TRUNC('year', :d) AS v_trunc_year, + DATE_TRUNC('quarter', :d) AS v_trunc_quarter, + DATE_TRUNC('month', :d) AS v_trunc_month, + DATE_TRUNC('day', :d) AS v_trunc_day, + DATE_TRUNC('hour', :d) AS v_trunc_hour, + DATE_TRUNC('minute', :d) AS v_trunc_minute, + DATE_TRUNC('second', :d) AS v_trunc_second, DATE_TRUNC('month', CAST(NOW() AS timestamp)) + 1 * INTERVAL '1 month' + -1 * INTERVAL '1 day' AS last_date_of_month, DATE_TRUNC('month', CAST(NOW() AS timestamp)) + INTERVAL '1 month - 1 day' AS last_date_of_month2 FROM diff --git a/test/Carbunql.TypeSafe.Test/WhereTest.cs b/test/Carbunql.TypeSafe.Test/WhereTest.cs index 1136a2fd..69ac87cf 100644 --- a/test/Carbunql.TypeSafe.Test/WhereTest.cs +++ b/test/Carbunql.TypeSafe.Test/WhereTest.cs @@ -47,14 +47,14 @@ public void WhereStaticVariable() Output.WriteLine(query.ToText()); var expect = @"/* - :p0 = 1 + :id = 1 */ SELECT * FROM sale AS a WHERE - a.sale_id = CAST(:p0 AS integer)"; + a.sale_id = CAST(:id AS integer)"; Assert.Equal(expect, actual, true, true, true); } @@ -215,14 +215,14 @@ public void AnyTest() Output.WriteLine(query.ToText()); var expect = @"/* - :p0 = System.Collections.Generic.List`1[System.Int32] + :id_array = System.Collections.Generic.List`1[System.Int32] */ SELECT * FROM sale AS a WHERE - a.sale_id = ANY(:p0)"; + a.sale_id = ANY(:id_array)"; Assert.Equal(expect, actual, true, true, true); } @@ -241,14 +241,14 @@ public void AnyTest_Right() Output.WriteLine(query.ToText()); var expect = @"/* - :p0 = System.Collections.Generic.List`1[System.Int32] + :id_array = System.Collections.Generic.List`1[System.Int32] */ SELECT * FROM sale AS a WHERE - a.sale_id = ANY(:p0)"; + a.sale_id = ANY(:id_array)"; Assert.Equal(expect, actual, true, true, true); }