diff --git a/src/Carbunql.TypeSafe/Extensions/ExpresionExtension.cs b/src/Carbunql.TypeSafe/Extensions/ExpresionExtension.cs new file mode 100644 index 00000000..935c9176 --- /dev/null +++ b/src/Carbunql.TypeSafe/Extensions/ExpresionExtension.cs @@ -0,0 +1,44 @@ +using System.Linq.Expressions; + +namespace Carbunql.TypeSafe.Extensions; + +public static class ExpresionExtension +{ + public static string ToValue(this Expression exp, Func addParameter) + { + if (exp is MemberExpression mem) + { + return mem.ToValue(ToValue, addParameter); + } + else if (exp is ConstantExpression ce) + { + return ce.ToValue(ToValue, addParameter); + } + else if (exp is NewExpression ne) + { + return ne.ToValue(ToValue, addParameter); + } + else if (exp is BinaryExpression be) + { + return be.ToValue(ToValue, addParameter); + } + else if (exp is UnaryExpression ue) + { + return ue.ToValue(ToValue, addParameter); + } + else if (exp is MethodCallExpression mce) + { + return mce.ToValue(ToValue, addParameter); + } + else if (exp is ConditionalExpression cnd) + { + 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.TypeSafe/Extensions/MethodCallExpressionExtension.cs b/src/Carbunql.TypeSafe/Extensions/MethodCallExpressionExtension.cs index ea6c68ae..4e53637f 100644 --- a/src/Carbunql.TypeSafe/Extensions/MethodCallExpressionExtension.cs +++ b/src/Carbunql.TypeSafe/Extensions/MethodCallExpressionExtension.cs @@ -2,6 +2,7 @@ using Carbunql.Annotations; using Carbunql.Building; using Carbunql.Extensions; +using Carbunql.TypeSafe.Extensions; using Carbunql.Values; using System.Collections; using System.Linq.Expressions; @@ -133,52 +134,8 @@ private static string CreateSqlCommand(this MethodCallExpression mce 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.RowNumberOrderBy): - if (mce.Arguments.First() is NewExpression argOrderbyBy) - { - var argOrderbyByText = string.Join(",", argOrderbyBy.Arguments.Select(x => mainConverter(x, addParameter))); - return DbmsConfiguration.GetRowNumberOrderByCommandLogic(argOrderbyByText); - } - break; + return Aggregate(mce, mainConverter, addParameter, "row_number"); - 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; case nameof(Sql.Exists): case nameof(Sql.NotExists): @@ -205,35 +162,141 @@ private static string CreateSqlCommand(this MethodCallExpression mce } private static string Aggregate(MethodCallExpression mce - , Func, string> mainConverter - , Func addParameter - , string aggregateFunction) + , Func, string> mainConverter + , Func addParameter + , string aggregateFunction) { #if DEBUG + // Analyze the expression tree for debugging purposes var analyze = ExpressionReader.Analyze(mce); #endif + // Extract the main aggregate function + string value; if (aggregateFunction.IsEqualNoCase("count")) { - return $"{aggregateFunction}(*)"; + value = "count(*)"; + } + else if (aggregateFunction.IsEqualNoCase("row_number")) + { + value = "row_number()"; + } + else + { + value = ExtractFunction(mce, mainConverter, addParameter, aggregateFunction, mce.Arguments[0]); + } + + // Determine the argument indices for partition and order + int partitionArgumentIndex = (aggregateFunction.IsEqualNoCase("count") || aggregateFunction.IsEqualNoCase("row_number")) ? 0 : 1; + int orderArgumentIndex = partitionArgumentIndex + 1; + + // Extract the partition and order clauses + // The arguments can be: + // - Main function argument, partition, order + // - Partition, order + // There are no functions that only have a partition or only have an order. + string partitionby = mce.Arguments.Count <= partitionArgumentIndex ? string.Empty : ExtractPartition(mce, mainConverter, addParameter, mce.Arguments[partitionArgumentIndex]); + string orderby = mce.Arguments.Count <= orderArgumentIndex ? string.Empty : ExtractOrder(mce, mainConverter, addParameter, mce.Arguments[orderArgumentIndex]); + + // Construct the final SQL function string with the over clause + if (!string.IsNullOrEmpty(partitionby) && !string.IsNullOrEmpty(orderby)) + { + value += $" over({partitionby} {orderby})"; + } + else if (!string.IsNullOrEmpty(partitionby)) + { + value += $" over({partitionby})"; + } + else if (!string.IsNullOrEmpty(orderby)) + { + value += $" over({orderby})"; } - var ue = (UnaryExpression)mce.Arguments[0]; + return value; + } + + private static string ExtractFunction(MethodCallExpression mce + , Func, string> mainConverter + , Func addParameter + , string functionName + , Expression? argument) + { + if (argument == null) throw new NotSupportedException(); + + var ue = (UnaryExpression)argument; var expression = (LambdaExpression)ue.Operand; if (expression.Body is BinaryExpression be) { var value = be.ToValue(mainConverter, addParameter); - return $"{aggregateFunction}({value})"; + return $"{functionName}({value})"; } if (expression.Body is MemberExpression me) { var value = me.ToValue(mainConverter, addParameter); - return $"{aggregateFunction}({value})"; + return $"{functionName}({value})"; } throw new NotSupportedException(); + } + + private static string ExtractPartition(MethodCallExpression mce + , Func, string> mainConverter + , Func addParameter + , Expression argument) + { + var functionName = "partition by"; + + var ue = (UnaryExpression)argument; + var expression = (LambdaExpression)ue.Operand; + + if (expression.Body is ConstantExpression) return string.Empty; + + if (expression.Body is NewExpression ne && ne.Members != null) + { + var value = string.Join(",", ne.Arguments.Select(x => x.ToValue(addParameter))); + return $"{functionName} {value}"; + } + + throw new NotSupportedException(); + } + private static string ExtractOrder(MethodCallExpression mce + , Func, string> mainConverter + , Func addParameter + , Expression argument) + { + var functionName = "order by"; + + var ue = (UnaryExpression)argument; + var expression = (LambdaExpression)ue.Operand; + + if (expression.Body is ConstantExpression) return string.Empty; + + if (expression.Body is NewExpression ne && ne.Members != null) + { + var cnt = ne.Members.Count(); + var args = new List() { Capacity = cnt }; + + // If an alias is specified, it is determined to be in "descending order". + for (var i = 0; i < cnt; i++) + { + var alias = ne.Members[i].Name; + var val = ne.Arguments[i].ToValue(addParameter); + if (ValueParser.Parse(val).GetDefaultName() == alias) + { + args.Add(val); + } + else + { + args.Add($"{val} desc"); + } + } + var value = string.Join(",", args); + return $"{functionName} {value}"; + } + + throw new NotSupportedException(); } private static string ToExistsClause(MethodCallExpression mce) @@ -247,7 +310,7 @@ private static string ToExistsClause(MethodCallExpression mce) var fsql = new FluentSelectQuery(); var (f, x) = fsql.From(clause).As(expression.Parameters[0].Name!); var prmManager = new ParameterManager(fsql.GetParameters(), fsql.AddParameter); - var value = fsql.ToValue(expression.Body, prmManager.AddParameter); + var value = expression.Body.ToValue(prmManager.AddParameter); fsql.Where(value); if (mce.Method.Name == nameof(Sql.Exists)) diff --git a/src/Carbunql.TypeSafe/FluentSelectQuery.cs b/src/Carbunql.TypeSafe/FluentSelectQuery.cs index 0e6d3588..575ba61a 100644 --- a/src/Carbunql.TypeSafe/FluentSelectQuery.cs +++ b/src/Carbunql.TypeSafe/FluentSelectQuery.cs @@ -9,7 +9,6 @@ using Carbunql.Values; using System.Data; using System.Linq.Expressions; -using System.Text.RegularExpressions; namespace Carbunql.TypeSafe; @@ -76,7 +75,7 @@ public FluentSelectQuery Select(Expression> expression) where T : cla } //add - var value = ToValue(ne.Arguments[i], prmManager.AddParameter); + var value = ne.Arguments[i].ToValue(prmManager.AddParameter); this.Select(RemoveRootBracketOrDefault(value)).As(alias); } return this; @@ -86,16 +85,6 @@ public FluentSelectQuery Select(Expression> expression) where T : cla throw new InvalidProgramException(); } - public FluentSelectQuery Count(Expression> expression) where T : class - { - return Aggregate(expression, "count"); - } - - public FluentSelectQuery Sum(Expression> expression) where T : class - { - return Aggregate(expression, "sum"); - } - private FluentSelectQuery Aggregate(Expression> expression, string aggregateFunction) where T : class { #if DEBUG @@ -113,7 +102,7 @@ private FluentSelectQuery Aggregate(Expression> expression, string ag for (var i = 0; i < cnt; i++) { var alias = ne.Members[i].Name; - var value = ToValue(ne.Arguments[i], prmManager.AddParameter); + var value = ne.Arguments[i].ToValue(prmManager.AddParameter); this.Select($"{aggregateFunction}({value})").As(alias); } return this; @@ -138,8 +127,7 @@ public FluentSelectQuery GroupBy(Expression> expression) where T : cl var cnt = ne.Members.Count(); for (var i = 0; i < cnt; i++) { - var alias = ne.Members[i].Name; - var value = ToValue(ne.Arguments[i], prmManager.AddParameter); + var value = ne.Arguments[i].ToValue(prmManager.AddParameter); var s = ValueParser.Parse(RemoveRootBracketOrDefault(value)); this.Group(s); } @@ -163,7 +151,7 @@ public FluentSelectQuery InnerJoin(Expression> tableExpression, Expre var table = compiledExpression(); var prmManager = new ParameterManager(GetParameters(), AddParameter); - var condition = ToValue(conditionExpression.Body, prmManager.AddParameter); + var condition = conditionExpression.Body.ToValue(prmManager.AddParameter); table.DataSet.BuildJoinClause(this, "inner join", tableAlias, condition); @@ -183,7 +171,7 @@ public FluentSelectQuery LeftJoin(Expression> tableExpression, Expres var table = compiledExpression(); var prmManager = new ParameterManager(GetParameters(), AddParameter); - var condition = ToValue(conditionExpression.Body, prmManager.AddParameter); + var condition = conditionExpression.Body.ToValue(prmManager.AddParameter); table.DataSet.BuildJoinClause(this, "left join", tableAlias, condition); @@ -203,7 +191,7 @@ public FluentSelectQuery RightJoin(Expression> tableExpression, Expre var table = compiledExpression(); var prmManager = new ParameterManager(GetParameters(), AddParameter); - var condition = ToValue(conditionExpression.Body, prmManager.AddParameter); + var condition = conditionExpression.Body.ToValue(prmManager.AddParameter); table.DataSet.BuildJoinClause(this, "right join", tableAlias, condition); @@ -232,7 +220,7 @@ public FluentSelectQuery Where(Expression> expression) var prmManager = new ParameterManager(GetParameters(), AddParameter); - var value = ToValue(expression.Body, prmManager.AddParameter); + var value = expression.Body.ToValue(prmManager.AddParameter); this.Where(value); @@ -245,7 +233,7 @@ public FluentSelectQuery Where(Expression> expression) var analyzed = ExpressionReader.Analyze(expression); #endif var prmManager = new ParameterManager(GetParameters(), AddParameter); - var value = ToValue(expression.Body, prmManager.AddParameter); + var value = expression.Body.ToValue(prmManager.AddParameter); var clause = TableDefinitionClauseFactory.Create(); @@ -264,7 +252,7 @@ public FluentSelectQuery Where(Expression> expression) var analyzed = ExpressionReader.Analyze(expression); #endif var prmManager = new ParameterManager(GetParameters(), AddParameter); - var value = ToValue(expression.Body, prmManager.AddParameter); + var value = expression.Body.ToValue(prmManager.AddParameter); var clause = TableDefinitionClauseFactory.Create(); @@ -285,7 +273,7 @@ public FluentSelectQuery Where(Expression> expression) var analyzed = ExpressionReader.Analyze(expression); #endif var prmManager = new ParameterManager(GetParameters(), AddParameter); - var value = ToValue(expression.Body, prmManager.AddParameter); + var value = expression.Body.ToValue(prmManager.AddParameter); var fsql = new SelectQuery(); fsql.From(dataset).As(expression.Parameters[0].Name!); @@ -296,44 +284,6 @@ public FluentSelectQuery Where(Expression> expression) return this; } - internal string ToValue(Expression exp, Func addParameter) - { - if (exp is MemberExpression mem) - { - return mem.ToValue(ToValue, addParameter); - } - else if (exp is ConstantExpression ce) - { - return ce.ToValue(ToValue, addParameter); - } - else if (exp is NewExpression ne) - { - return ne.ToValue(ToValue, addParameter); - } - else if (exp is BinaryExpression be) - { - return be.ToValue(ToValue, addParameter); - } - else if (exp is UnaryExpression ue) - { - return ue.ToValue(ToValue, addParameter); - } - else if (exp is MethodCallExpression mce) - { - return mce.ToValue(ToValue, addParameter); - } - else if (exp is ConditionalExpression cnd) - { - return cnd.ToValue(ToValue, addParameter); - } - else if (exp is ParameterExpression prm) - { - return prm.ToValue(ToValue, addParameter); - } - - throw new InvalidProgramException(exp.ToString()); - } - internal static string CreateCastStatement(string value, Type type) { var dbtype = DbmsConfiguration.ToDbType(type); diff --git a/src/Carbunql.TypeSafe/Sql.cs b/src/Carbunql.TypeSafe/Sql.cs index c820cd68..841f9192 100644 --- a/src/Carbunql.TypeSafe/Sql.cs +++ b/src/Carbunql.TypeSafe/Sql.cs @@ -153,21 +153,27 @@ public static string Raw(string command) public static DateTime DateTruncateToSecond(DateTime d) => new DateTime(); - public static string RowNumber() => string.Empty; + public static int RowNumber() => 0; - public static string RowNumber(object partition, object order) => string.Empty; - - public static string RowNumberPartitionBy(object partition) => string.Empty; - - public static string RowNumberOrderBy(object order) => string.Empty; + public static int RowNumber(Expression> partition, Expression> order) => throw new InvalidProgramException(); public static int Count() => throw new InvalidProgramException(); + public static int Count(Expression> partition, Expression> order) => throw new InvalidProgramException(); + public static T Sum(Expression> expression) => throw new InvalidProgramException(); + public static T Sum(Expression> expression, Expression> partition, Expression> order) => throw new InvalidProgramException(); + public static T Average(Expression> expression) => throw new InvalidProgramException(); + public static T Average(Expression> expression, Expression> partition, Expression> order) => throw new InvalidProgramException(); + public static T Max(Expression> expression) => throw new InvalidProgramException(); + public static T Max(Expression> expression, Expression> partition, Expression> order) => throw new InvalidProgramException(); + public static T Min(Expression> expression) => throw new InvalidProgramException(); + + public static T Min(Expression> expression, Expression> partition, Expression> order) => throw new InvalidProgramException(); } diff --git a/test/Carbunql.TypeSafe.Test/GroupTest.cs b/test/Carbunql.TypeSafe.Test/GroupTest.cs index 1ac4c3e2..ff61846e 100644 --- a/test/Carbunql.TypeSafe.Test/GroupTest.cs +++ b/test/Carbunql.TypeSafe.Test/GroupTest.cs @@ -22,11 +22,11 @@ public void GroupBy() { od.order_id, total_price = Sql.Sum(() => od.quantity * od.price), - total_amount = Sql.Sum(() => od.quantity), + total_quantity = Sql.Sum(() => od.quantity), count = Sql.Count(), - avg_amount = Sql.Average(() => od.quantity), - min_amount = Sql.Min(() => od.quantity), - max_amount = Sql.Max(() => od.quantity), + avg_quantity = Sql.Average(() => od.quantity), + min_quantity = Sql.Min(() => od.quantity), + max_quantity = Sql.Max(() => od.quantity), }) .GroupBy(() => new { @@ -39,11 +39,11 @@ public void GroupBy() var expect = @"SELECT od.order_id, SUM(CAST(od.quantity AS numeric) * od.price) AS total_price, - SUM(od.quantity) AS total_amount, + SUM(od.quantity) AS total_quantity, COUNT(*) AS count, - AVG(od.quantity) AS avg_amount, - MIN(od.quantity) AS min_amount, - MAX(od.quantity) AS max_amount + AVG(od.quantity) AS avg_quantity, + MIN(od.quantity) AS min_quantity, + MAX(od.quantity) AS max_quantity FROM order_detail AS od GROUP BY diff --git a/test/Carbunql.TypeSafe.Test/SingleTableTest.cs b/test/Carbunql.TypeSafe.Test/SingleTableTest.cs index 9e0ae4cc..8001e7f1 100644 --- a/test/Carbunql.TypeSafe.Test/SingleTableTest.cs +++ b/test/Carbunql.TypeSafe.Test/SingleTableTest.cs @@ -386,7 +386,7 @@ current_timestamp AS rawcommand } [Fact] - public void ReservedCommand() + public void Now_Timestamp() { var a = Sql.DefineDataSet(); @@ -395,10 +395,6 @@ public void ReservedCommand() { 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.RowNumberOrderBy(new { a.product_name, a.unit_price }) }); var actual = query.ToText(); @@ -406,26 +402,7 @@ public void ReservedCommand() var expect = @"SELECT CAST(NOW() AS timestamp) 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 + current_timestamp AS timestamp_commend FROM sale AS a"; diff --git a/test/Carbunql.TypeSafe.Test/WindowFunctionTest.cs b/test/Carbunql.TypeSafe.Test/WindowFunctionTest.cs new file mode 100644 index 00000000..5324c7a7 --- /dev/null +++ b/test/Carbunql.TypeSafe.Test/WindowFunctionTest.cs @@ -0,0 +1,376 @@ +using Xunit.Abstractions; + +namespace Carbunql.TypeSafe.Test; + +public class WindowFunctionTest +{ + public WindowFunctionTest(ITestOutputHelper output) + { + Output = output; + } + + private ITestOutputHelper Output { get; } + + [Fact] + public void RowNumber() + { + var od = Sql.DefineDataSet(); + + var query = Sql.From(() => od) + .Select(() => new + { + od.order_detail_id, + no_argument = Sql.RowNumber(), + partition_only = Sql.RowNumber( + () => new { od.order_id }, + () => null + ), + order_only = Sql.RowNumber( + () => null, + () => new { od.order_detail_id } + ), + partition_order = Sql.RowNumber( + () => new { od.order_id }, + () => new { od.order_detail_id } + ), + }); + + var actual = query.ToText(); + Output.WriteLine(actual); + + var expect = @"SELECT + od.order_detail_id, + ROW_NUMBER() AS no_argument, + ROW_NUMBER() OVER( + PARTITION BY + od.order_id + ) AS partition_only, + ROW_NUMBER() OVER( + ORDER BY + od.order_detail_id + ) AS order_only, + ROW_NUMBER() OVER( + PARTITION BY + od.order_id + ORDER BY + od.order_detail_id + ) AS partition_order +FROM + order_detail AS od"; + + Assert.Equal(expect, actual, true, true, true); + } + + [Fact] + public void Count() + { + var od = Sql.DefineDataSet(); + + var query = Sql.From(() => od) + .Select(() => new + { + od.order_detail_id, + no_argument = Sql.Count(), + partition_only = Sql.Count( + () => new { od.order_id }, + () => null + ), + order_only = Sql.Count( + () => null, + () => new { od.order_detail_id } + ), + partition_order = Sql.Count( + () => new { od.order_id }, + () => new { od.order_detail_id } + ), + }); + + var actual = query.ToText(); + Output.WriteLine(actual); + + var expect = @"SELECT + od.order_detail_id, + COUNT(*) AS no_argument, + COUNT(*) OVER( + PARTITION BY + od.order_id + ) AS partition_only, + COUNT(*) OVER( + ORDER BY + od.order_detail_id + ) AS order_only, + COUNT(*) OVER( + PARTITION BY + od.order_id + ORDER BY + od.order_detail_id + ) AS partition_order +FROM + order_detail AS od"; + + Assert.Equal(expect, actual, true, true, true); + } + + [Fact] + public void Sum() + { + var od = Sql.DefineDataSet(); + + var query = Sql.From(() => od) + .Select(() => new + { + od.order_detail_id, + no_argument = Sql.Sum(() => od.quantity), + partition_only = Sql.Sum( + () => od.quantity, + () => new { od.order_id }, + () => null + ), + order_only = Sql.Sum( + () => od.quantity, + () => null, + () => new { od.order_detail_id } + ), + partition_order = Sql.Sum( + () => od.quantity, + () => new { od.order_id }, + () => new { od.order_detail_id } + ), + }); + + var actual = query.ToText(); + Output.WriteLine(actual); + + var expect = @"SELECT + od.order_detail_id, + SUM(od.quantity) AS no_argument, + SUM(od.quantity) OVER( + PARTITION BY + od.order_id + ) AS partition_only, + SUM(od.quantity) OVER( + ORDER BY + od.order_detail_id + ) AS order_only, + SUM(od.quantity) OVER( + PARTITION BY + od.order_id + ORDER BY + od.order_detail_id + ) AS partition_order +FROM + order_detail AS od"; + + Assert.Equal(expect, actual, true, true, true); + } + + [Fact] + public void Min() + { + var od = Sql.DefineDataSet(); + + var query = Sql.From(() => od) + .Select(() => new + { + od.order_detail_id, + no_argument = Sql.Min(() => od.quantity), + partition_only = Sql.Min( + () => od.quantity, + () => new { od.order_id }, + () => null + ), + order_only = Sql.Min( + () => od.quantity, + () => null, + () => new { od.order_detail_id } + ), + partition_order = Sql.Min( + () => od.quantity, + () => new { od.order_id }, + () => new { od.order_detail_id } + ), + }); + + var actual = query.ToText(); + Output.WriteLine(actual); + + var expect = @"SELECT + od.order_detail_id, + MIN(od.quantity) AS no_argument, + MIN(od.quantity) OVER( + PARTITION BY + od.order_id + ) AS partition_only, + MIN(od.quantity) OVER( + ORDER BY + od.order_detail_id + ) AS order_only, + MIN(od.quantity) OVER( + PARTITION BY + od.order_id + ORDER BY + od.order_detail_id + ) AS partition_order +FROM + order_detail AS od"; + + Assert.Equal(expect, actual, true, true, true); + } + + [Fact] + public void Max() + { + var od = Sql.DefineDataSet(); + + var query = Sql.From(() => od) + .Select(() => new + { + od.order_detail_id, + no_argument = Sql.Max(() => od.quantity), + partition_only = Sql.Max( + () => od.quantity, + () => new { od.order_id }, + () => null + ), + order_only = Sql.Max( + () => od.quantity, + () => null, + () => new { od.order_detail_id } + ), + partition_order = Sql.Max( + () => od.quantity, + () => new { od.order_id }, + () => new { od.order_detail_id } + ), + }); + + var actual = query.ToText(); + Output.WriteLine(actual); + + var expect = @"SELECT + od.order_detail_id, + MAX(od.quantity) AS no_argument, + MAX(od.quantity) OVER( + PARTITION BY + od.order_id + ) AS partition_only, + MAX(od.quantity) OVER( + ORDER BY + od.order_detail_id + ) AS order_only, + MAX(od.quantity) OVER( + PARTITION BY + od.order_id + ORDER BY + od.order_detail_id + ) AS partition_order +FROM + order_detail AS od"; + + Assert.Equal(expect, actual, true, true, true); + } + + [Fact] + public void Average() + { + var od = Sql.DefineDataSet(); + + var query = Sql.From(() => od) + .Select(() => new + { + od.order_detail_id, + no_argument = Sql.Average(() => od.quantity), + partition_only = Sql.Average( + () => od.quantity, + () => new { od.order_id }, + () => null + ), + order_only = Sql.Average( + () => od.quantity, + () => null, + () => new { od.order_detail_id } + ), + partition_order = Sql.Average( + () => od.quantity, + () => new { od.order_id }, + () => new { od.order_detail_id } + ), + }); + + var actual = query.ToText(); + Output.WriteLine(actual); + + var expect = @"SELECT + od.order_detail_id, + AVG(od.quantity) AS no_argument, + AVG(od.quantity) OVER( + PARTITION BY + od.order_id + ) AS partition_only, + AVG(od.quantity) OVER( + ORDER BY + od.order_detail_id + ) AS order_only, + AVG(od.quantity) OVER( + PARTITION BY + od.order_id + ORDER BY + od.order_detail_id + ) AS partition_order +FROM + order_detail AS od"; + + Assert.Equal(expect, actual, true, true, true); + + } + public record product : IDataRow + { + public int product_id { get; set; } + public string name { get; set; } = string.Empty; + public decimal price { get; set; } + + // interface property + IDataSet IDataRow.DataSet { get; set; } = null!; + } + + public record store : IDataRow + { + public int store_id { get; set; } + public string name { get; set; } = string.Empty; + public string location { get; set; } = string.Empty; + + // interface property + IDataSet IDataRow.DataSet { get; set; } = null!; + } + + public record order : IDataRow + { + public int order_id { get; set; } + public DateTime order_date { get; set; } + public string customer_name { get; set; } = string.Empty; + public int store_id { get; set; } + public IList order_details { get; init; } = new List(); + + // interface property + IDataSet IDataRow.DataSet { get; set; } = null!; + } + + public record order_detail : IDataRow + { + public int order_detail_id { get; set; } + public int order_id { get; set; } + public int product_id { get; set; } + public int quantity { get; set; } + public decimal price { get; set; } + + // interface property + IDataSet IDataRow.DataSet { get; set; } = null!; + } + + public record order_detail_report : order_detail + { + public DateTime order_date { get; set; } + public string customer_name { get; set; } = string.Empty; + public int store_id { get; set; } + } +}