diff --git a/QueryBuilder.Tests/AggregateTests.cs b/QueryBuilder.Tests/AggregateTests.cs index 68a69842..9db3d7b8 100644 --- a/QueryBuilder.Tests/AggregateTests.cs +++ b/QueryBuilder.Tests/AggregateTests.cs @@ -1,5 +1,6 @@ using SqlKata.Compilers; using SqlKata.Tests.Infrastructure; +using System; using Xunit; namespace SqlKata.Tests @@ -7,9 +8,159 @@ namespace SqlKata.Tests public class AggregateTests : TestSupport { [Fact] - public void Count() + public void SelectAggregateEmpty() { - var query = new Query("A").AsCount(); + Assert.Throws(() => new Query("A").SelectAggregate("aggregate", new string[] { })); + } + + [Fact] + public void SelectAggregate() + { + var query = new Query("A").SelectAggregate("aggregate", new[] { "Column" }); + + var c = Compile(query); + + Assert.Equal("SELECT AGGREGATE([Column]) AS [aggregate] FROM [A]", c[EngineCodes.SqlServer]); + Assert.Equal("SELECT AGGREGATE(`Column`) AS `aggregate` FROM `A`", c[EngineCodes.MySql]); + Assert.Equal("SELECT AGGREGATE(\"Column\") AS \"aggregate\" FROM \"A\"", c[EngineCodes.PostgreSql]); + Assert.Equal("SELECT AGGREGATE(\"COLUMN\") AS \"AGGREGATE\" FROM \"A\"", c[EngineCodes.Firebird]); + } + + [Fact] + public void SelectAggregateAlias() + { + var query = new Query("A").SelectAggregate("aggregate", new[] { "Column" }, "Alias"); + + var c = Compile(query); + + Assert.Equal("SELECT AGGREGATE([Column]) AS [Alias] FROM [A]", c[EngineCodes.SqlServer]); + Assert.Equal("SELECT AGGREGATE(`Column`) AS `Alias` FROM `A`", c[EngineCodes.MySql]); + Assert.Equal("SELECT AGGREGATE(\"Column\") AS \"Alias\" FROM \"A\"", c[EngineCodes.PostgreSql]); + Assert.Equal("SELECT AGGREGATE(\"COLUMN\") AS \"ALIAS\" FROM \"A\"", c[EngineCodes.Firebird]); + } + + [Fact] + public void SelectAggregateMultipleColumns() + { + Assert.Throws(() => + new Query("A").SelectAggregate("aggregate", new[] { "Column1", "Column2" }) + ); + } + + [Fact] + public void SelectAggregateMultipleColumnsAlias() + { + Assert.Throws(() => + new Query("A").SelectAggregate("aggregate", new[] { "Column1", "Column2" }, "Alias") + ); + } + + [Fact] + public void MultipleAggregatesPerQuery() + { + var query = new Query() + .SelectMin("MinColumn") + .SelectMax("MaxColumn") + .From("Table") + ; + + var c = Compile(query); + + Assert.Equal("SELECT MIN([MinColumn]) AS [min], MAX([MaxColumn]) AS [max] FROM [Table]", c[EngineCodes.SqlServer]); + Assert.Equal("SELECT MIN(`MinColumn`) AS `min`, MAX(`MaxColumn`) AS `max` FROM `Table`", c[EngineCodes.MySql]); + Assert.Equal("SELECT MIN(\"MINCOLUMN\") AS \"MIN\", MAX(\"MAXCOLUMN\") AS \"MAX\" FROM \"TABLE\"", c[EngineCodes.Firebird]); + Assert.Equal("SELECT MIN(\"MinColumn\") AS \"min\", MAX(\"MaxColumn\") AS \"max\" FROM \"Table\"", c[EngineCodes.PostgreSql]); + } + + [Fact] + public void AggregatesAndNonAggregatesCanBeMixedInQueries1() + { + var query = new Query() + .Select("ColumnA") + .SelectMax("ColumnB") + .From("Table") + ; + + var c = Compile(query); + + Assert.Equal("SELECT [ColumnA], MAX([ColumnB]) AS [max] FROM [Table]", c[EngineCodes.SqlServer]); + Assert.Equal("SELECT `ColumnA`, MAX(`ColumnB`) AS `max` FROM `Table`", c[EngineCodes.MySql]); + Assert.Equal("SELECT \"COLUMNA\", MAX(\"COLUMNB\") AS \"MAX\" FROM \"TABLE\"", c[EngineCodes.Firebird]); + Assert.Equal("SELECT \"ColumnA\", MAX(\"ColumnB\") AS \"max\" FROM \"Table\"", c[EngineCodes.PostgreSql]); + } + + [Fact] + public void AggregatesAndNonAggregatesCanBeMixedInQueries2() + { + var query = new Query() + .SelectMax("ColumnA") + .Select("ColumnB") + .From("Table") + ; + + var c = Compile(query); + + Assert.Equal("SELECT MAX([ColumnA]) AS [max], [ColumnB] FROM [Table]", c[EngineCodes.SqlServer]); + Assert.Equal("SELECT MAX(`ColumnA`) AS `max`, `ColumnB` FROM `Table`", c[EngineCodes.MySql]); + Assert.Equal("SELECT MAX(\"COLUMNA\") AS \"MAX\", \"COLUMNB\" FROM \"TABLE\"", c[EngineCodes.Firebird]); + Assert.Equal("SELECT MAX(\"ColumnA\") AS \"max\", \"ColumnB\" FROM \"Table\"", c[EngineCodes.PostgreSql]); + } + + [Fact] + public void AggregatesCanHaveALimit() + { + var query = new Query() + .SelectMin("ColumnA", "MinValue") + .SelectMax("ColumnB", "MaxValue") + .From("Table") + .Limit(100) + ; + + var c = Compile(query); + + Assert.Equal("SELECT TOP (100) MIN([ColumnA]) AS [MinValue], MAX([ColumnB]) AS [MaxValue] FROM [Table]", c[EngineCodes.SqlServer]); + Assert.Equal("SELECT FIRST 100 MIN(\"COLUMNA\") AS \"MINVALUE\", MAX(\"COLUMNB\") AS \"MAXVALUE\" FROM \"TABLE\"", c[EngineCodes.Firebird]); + Assert.Equal("SELECT MIN(`ColumnA`) AS `MinValue`, MAX(`ColumnB`) AS `MaxValue` FROM `Table` LIMIT 100", c[EngineCodes.MySql]); + } + + [Fact] + public void AggregatesCanHaveAnOrderBy() + { + var query = new Query() + .SelectMin("ColumnA", "MinValue") + .SelectMax("ColumnB", "MaxValue") + .From("Table") + .OrderBy("MinValue") + ; + + var c = Compile(query); + + Assert.Equal("SELECT MIN([ColumnA]) AS [MinValue], MAX([ColumnB]) AS [MaxValue] FROM [Table] ORDER BY [MinValue]", c[EngineCodes.SqlServer]); + Assert.Equal("SELECT MIN(\"COLUMNA\") AS \"MINVALUE\", MAX(\"COLUMNB\") AS \"MAXVALUE\" FROM \"TABLE\" ORDER BY \"MINVALUE\"", c[EngineCodes.Firebird]); + Assert.Equal("SELECT MIN(`ColumnA`) AS `MinValue`, MAX(`ColumnB`) AS `MaxValue` FROM `Table` ORDER BY `MinValue`", c[EngineCodes.MySql]); + } + + [Fact] + public void AggregatesCanHaveAGroupBy() + { + var query = new Query() + .SelectMin("ColumnA", "MinValue") + .SelectMax("ColumnB", "MaxValue") + .From("Table") + .GroupBy("MinValue") + ; + + var c = Compile(query); + + Assert.Equal("SELECT MIN([ColumnA]) AS [MinValue], MAX([ColumnB]) AS [MaxValue] FROM [Table] GROUP BY [MinValue]", c[EngineCodes.SqlServer]); + Assert.Equal("SELECT MIN(\"COLUMNA\") AS \"MINVALUE\", MAX(\"COLUMNB\") AS \"MAXVALUE\" FROM \"TABLE\" GROUP BY \"MINVALUE\"", c[EngineCodes.Firebird]); + Assert.Equal("SELECT MIN(`ColumnA`) AS `MinValue`, MAX(`ColumnB`) AS `MaxValue` FROM `Table` GROUP BY `MinValue`", c[EngineCodes.MySql]); + } + + [Fact] + public void SelectCount() + { + var query = new Query("A").SelectCount(); var c = Compile(query); @@ -19,74 +170,167 @@ public void Count() Assert.Equal("SELECT COUNT(*) AS \"COUNT\" FROM \"A\"", c[EngineCodes.Firebird]); } + [Fact] + public void SelectCountStarAlias() + { + var query = new Query("A").SelectCount("*", "Alias"); + + var c = Compile(query); + + Assert.Equal("SELECT COUNT(*) AS [Alias] FROM [A]", c[EngineCodes.SqlServer]); + Assert.Equal("SELECT COUNT(*) AS `Alias` FROM `A`", c[EngineCodes.MySql]); + Assert.Equal("SELECT COUNT(*) AS \"Alias\" FROM \"A\"", c[EngineCodes.PostgreSql]); + Assert.Equal("SELECT COUNT(*) AS \"ALIAS\" FROM \"A\"", c[EngineCodes.Firebird]); + } + + [Fact] + public void SelectCountColumnAlias() + { + var query = new Query("A").SelectCount("Column", "Alias"); + + var c = Compile(query); + + Assert.Equal("SELECT COUNT([Column]) AS [Alias] FROM [A]", c[EngineCodes.SqlServer]); + Assert.Equal("SELECT COUNT(`Column`) AS `Alias` FROM `A`", c[EngineCodes.MySql]); + Assert.Equal("SELECT COUNT(\"Column\") AS \"Alias\" FROM \"A\"", c[EngineCodes.PostgreSql]); + Assert.Equal("SELECT COUNT(\"COLUMN\") AS \"ALIAS\" FROM \"A\"", c[EngineCodes.Firebird]); + } + + [Fact] + public void SelectCountDoesntModifyColumns() + { + { + var columns = new string[] { }; + var query = new Query("A").SelectCount(columns); + Compile(query); + Assert.Equal(columns, new string[] { }); + } + { + var columns = new[] { "ColumnA", "ColumnB" }; + var query = new Query("A").SelectCount(columns); + Compile(query); + Assert.Equal(columns, new[] { "ColumnA", "ColumnB" }); + } + } + [Fact] public void CountMultipleColumns() { - var query = new Query("A").AsCount(new[] { "ColumnA", "ColumnB" }); + var query = new Query("A").SelectCount(new[] { "ColumnA", "ColumnB" }); + + var c = Compile(query); + + Assert.Equal("SELECT COUNT(*) AS [count] FROM (SELECT 1 FROM [A] WHERE [ColumnA] IS NOT NULL AND [ColumnB] IS NOT NULL) AS [CountQuery]", c[EngineCodes.SqlServer]); + } + + [Fact] + public void SelectCountMultipleColumns() + { + var query = new Query("A").SelectCount(new[] { "ColumnA", "ColumnB" }, "Alias"); var c = Compile(query); - Assert.Equal("SELECT COUNT(*) AS [count] FROM (SELECT 1 FROM [A] WHERE [ColumnA] IS NOT NULL AND [ColumnB] IS NOT NULL) AS [countQuery]", c[EngineCodes.SqlServer]); + Assert.Equal("SELECT COUNT(*) AS [Alias] FROM (SELECT 1 FROM [A] WHERE [ColumnA] IS NOT NULL AND [ColumnB] IS NOT NULL) AS [AliasCountQuery]", c[EngineCodes.SqlServer]); } [Fact] public void DistinctCount() { - var query = new Query("A").Distinct().AsCount(); + var query = new Query("A").Distinct().SelectCount(); var c = Compile(query); - Assert.Equal("SELECT COUNT(*) AS [count] FROM (SELECT DISTINCT * FROM [A]) AS [countQuery]", c[EngineCodes.SqlServer]); + Assert.Equal("SELECT COUNT(*) AS [count] FROM (SELECT DISTINCT * FROM [A]) AS [CountQuery]", c[EngineCodes.SqlServer]); } [Fact] public void DistinctCountMultipleColumns() { - var query = new Query("A").Distinct().AsCount(new[] { "ColumnA", "ColumnB" }); + var query = new Query("A").Distinct().SelectCount(new[] { "ColumnA", "ColumnB" }); var c = Compile(query); - Assert.Equal("SELECT COUNT(*) AS [count] FROM (SELECT DISTINCT [ColumnA], [ColumnB] FROM [A]) AS [countQuery]", c[EngineCodes.SqlServer]); + Assert.Equal("SELECT COUNT(*) AS [count] FROM (SELECT DISTINCT [ColumnA], [ColumnB] FROM [A]) AS [CountQuery]", c[EngineCodes.SqlServer]); } [Fact] public void Average() { - var query = new Query("A").AsAverage("TTL"); + var query = new Query("A").SelectAverage("TTL"); var c = Compile(query); Assert.Equal("SELECT AVG([TTL]) AS [avg] FROM [A]", c[EngineCodes.SqlServer]); } + [Fact] + public void AverageAlias() + { + var query = new Query("A").SelectAverage("TTL", "Alias"); + + var c = Compile(query); + + Assert.Equal("SELECT AVG([TTL]) AS [Alias] FROM [A]", c[EngineCodes.SqlServer]); + } + [Fact] public void Sum() { - var query = new Query("A").AsSum("PacketsDropped"); + var query = new Query("A").SelectSum("PacketsDropped"); var c = Compile(query); Assert.Equal("SELECT SUM([PacketsDropped]) AS [sum] FROM [A]", c[EngineCodes.SqlServer]); } + [Fact] + public void SumAlias() + { + var query = new Query("A").SelectSum("PacketsDropped", "Alias"); + + var c = Compile(query); + + Assert.Equal("SELECT SUM([PacketsDropped]) AS [Alias] FROM [A]", c[EngineCodes.SqlServer]); + } + [Fact] public void Max() { - var query = new Query("A").AsMax("LatencyMs"); + var query = new Query("A").SelectMax("LatencyMs"); var c = Compile(query); Assert.Equal("SELECT MAX([LatencyMs]) AS [max] FROM [A]", c[EngineCodes.SqlServer]); } + [Fact] + public void MaxAlias() + { + var query = new Query("A").SelectMax("LatencyMs", "Alias"); + + var c = Compile(query); + + Assert.Equal("SELECT MAX([LatencyMs]) AS [Alias] FROM [A]", c[EngineCodes.SqlServer]); + } + [Fact] public void Min() { - var query = new Query("A").AsMin("LatencyMs"); + var query = new Query("A").SelectMin("LatencyMs"); var c = Compile(query); Assert.Equal("SELECT MIN([LatencyMs]) AS [min] FROM [A]", c[EngineCodes.SqlServer]); } + + [Fact] + public void MinAlias() + { + var query = new Query("A").SelectMin("LatencyMs", "Alias"); + + var c = Compile(query); + + Assert.Equal("SELECT MIN([LatencyMs]) AS [Alias] FROM [A]", c[EngineCodes.SqlServer]); + } } } diff --git a/QueryBuilder.Tests/DefineTest.cs b/QueryBuilder.Tests/DefineTest.cs index 0b5ff292..28ec9c3e 100644 --- a/QueryBuilder.Tests/DefineTest.cs +++ b/QueryBuilder.Tests/DefineTest.cs @@ -29,7 +29,7 @@ public void Test_Define_SubQuery() { var subquery = new Query("Products") - .AsAverage("unitprice") + .SelectAverage("unitprice") .Define("@UnitsInSt", 10) .Where("UnitsInStock", ">", Variable("@UnitsInSt")); @@ -252,7 +252,7 @@ public void Test_Define_NestedCondition() .Where(q => q.Where("ShipRegion", "!=", Variable("@shipReg")) // .WhereRaw("1 = @one") - ).AsCount(); + ).SelectCount(); var c = Compile(query); diff --git a/QueryBuilder.Tests/SelectTests.cs b/QueryBuilder.Tests/SelectTests.cs index 8341bf1f..0634a6df 100644 --- a/QueryBuilder.Tests/SelectTests.cs +++ b/QueryBuilder.Tests/SelectTests.cs @@ -35,6 +35,31 @@ public void BasicSelectEnumerable() Assert.Equal("SELECT \"id\", \"name\" FROM \"users\"", c[EngineCodes.Oracle]); } + public void SelectAs() + { + var query = new Query().SelectAs(("Row", "Alias")).From("Table"); + + var c = Compile(query); + Assert.Equal("SELECT [Row] AS [Alias] FROM [Table]", c[EngineCodes.SqlServer]); + Assert.Equal("SELECT `Row` AS `Alias` FROM `Table`", c[EngineCodes.MySql]); + Assert.Equal("SELECT \"Row\" AS \"Alias\" FROM \"Table\"", c[EngineCodes.PostgreSql]); + Assert.Equal("SELECT \"ROW\" AS \"ALIAS\" FROM \"TABLE\"", c[EngineCodes.Firebird]); + Assert.Equal("SELECT \"Row\" \"Alias\" FROM \"Table\"", c[EngineCodes.Oracle]); + } + + [Fact] + public void SelectAsMultipleColumns() + { + var query = new Query().SelectAs(("Row1", "Alias1"), ("Row2", "Alias2")).From("Table"); + + var c = Compile(query); + Assert.Equal("SELECT [Row1] AS [Alias1], [Row2] AS [Alias2] FROM [Table]", c[EngineCodes.SqlServer]); + Assert.Equal("SELECT `Row1` AS `Alias1`, `Row2` AS `Alias2` FROM `Table`", c[EngineCodes.MySql]); + Assert.Equal("SELECT \"Row1\" AS \"Alias1\", \"Row2\" AS \"Alias2\" FROM \"Table\"", c[EngineCodes.PostgreSql]); + Assert.Equal("SELECT \"ROW1\" AS \"ALIAS1\", \"ROW2\" AS \"ALIAS2\" FROM \"TABLE\"", c[EngineCodes.Firebird]); + Assert.Equal("SELECT \"Row1\" \"Alias1\", \"Row2\" \"Alias2\" FROM \"Table\"", c[EngineCodes.Oracle]); + } + [Fact] public void BasicSelectWhereBindingIsEmptyOrNull() { @@ -74,6 +99,20 @@ public void ExpandedSelect() Assert.Equal("SELECT `users`.`id`, `users`.`name`, `users`.`age` FROM `users`", c[EngineCodes.MySql]); } + [Fact] + public void ExpandedSelectAs() + { + var q = new Query().From("users").SelectAs(("users.{id,name, age}", "Alias")); + var c = Compile(q); + + // This result is weird (but valid syntax), and at least it works in + // a somewhat explainable way. The regular 'as' keyword support in + // Select() does not work when combined with the {...}-expansion + // syntax (the alias will be lost). + Assert.Equal("SELECT [users].[id] AS [Alias], [users].[name] AS [Alias], [users].[age] AS [Alias] FROM [users]", c[EngineCodes.SqlServer]); + Assert.Equal("SELECT `users`.`id` AS `Alias`, `users`.`name` AS `Alias`, `users`.`age` AS `Alias` FROM `users`", c[EngineCodes.MySql]); + } + [Fact] public void ExpandedSelectWithSchema() { @@ -165,7 +204,7 @@ public void OrWhereNull() [Fact] public void WhereSub() { - var subQuery = new Query("Table2").WhereColumns("Table2.Column", "=", "Table.MyCol").AsCount(); + var subQuery = new Query("Table2").WhereColumns("Table2.Column", "=", "Table.MyCol").SelectCount(); var query = new Query("Table").WhereSub(subQuery, 1); @@ -179,7 +218,7 @@ public void WhereSub() [Fact] public void OrWhereSub() { - var subQuery = new Query("Table2").WhereColumns("Table2.Column", "=", "Table.MyCol").AsCount(); + var subQuery = new Query("Table2").WhereColumns("Table2.Column", "=", "Table.MyCol").SelectCount(); var query = new Query("Table").WhereNull("MyCol").OrWhereSub(subQuery, "<", 1); diff --git a/QueryBuilder/Clauses/AggregateClause.cs b/QueryBuilder/Clauses/AggregateClause.cs index 2d18d78e..1629db6d 100644 --- a/QueryBuilder/Clauses/AggregateClause.cs +++ b/QueryBuilder/Clauses/AggregateClause.cs @@ -16,6 +16,11 @@ public class AggregateClause : AbstractClause /// public List Columns { get; set; } + /// + /// Gets or sets the alias of the result column. + /// + public string Alias { get; set; } + /// /// Gets or sets the type of aggregate function. /// @@ -32,6 +37,7 @@ public override AbstractClause Clone() Engine = Engine, Type = Type, Columns = new List(Columns), + Alias = Alias, Component = Component, }; } diff --git a/QueryBuilder/Clauses/ColumnClause.cs b/QueryBuilder/Clauses/ColumnClause.cs index 58872d8e..2822e051 100644 --- a/QueryBuilder/Clauses/ColumnClause.cs +++ b/QueryBuilder/Clauses/ColumnClause.cs @@ -1,7 +1,11 @@ +using System.Collections.Generic; +using System.Diagnostics; + namespace SqlKata { public abstract class AbstractColumn : AbstractClause { + public string Alias { get; set; } } /// @@ -26,6 +30,7 @@ public override AbstractClause Clone() Engine = Engine, Name = Name, Component = Component, + Alias = Alias, }; } } @@ -50,6 +55,24 @@ public override AbstractClause Clone() Engine = Engine, Query = Query.Clone(), Component = Component, + Alias = Alias, + }; + } + } + + public class AggregateColumn : AbstractColumn + { + public string Type { get; set; } // Min, Max, etc. + public string Column { get; set; } // Aggregate functions accept only a single 'value expression' (for now we implement only column name) + public override AbstractClause Clone() + { + return new AggregateColumn + { + Engine = Engine, + Component = Component, + Alias = Alias, + Type = Type, + Column = Column, }; } } @@ -68,6 +91,7 @@ public class RawColumn : AbstractColumn /// public override AbstractClause Clone() { + Debug.Assert(string.IsNullOrEmpty(Alias), "Raw columns cannot have an alias"); return new RawColumn { Engine = Engine, diff --git a/QueryBuilder/Compilers/Compiler.cs b/QueryBuilder/Compilers/Compiler.cs index 28abe5a6..fb822ff2 100644 --- a/QueryBuilder/Compilers/Compiler.cs +++ b/QueryBuilder/Compilers/Compiler.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using System.Text; @@ -69,7 +70,7 @@ private Query TransformAggregateQuery(Query query) { query.ClearComponent("aggregate", EngineCode); query.ClearComponent("select", EngineCode); - query.Select(clause.Columns.ToArray()); + query.SelectAs(clause.Columns.Select(x => (x, null as string)).ToArray()); } else { @@ -82,12 +83,14 @@ private Query TransformAggregateQuery(Query query) var outerClause = new AggregateClause() { Columns = new List { "*" }, - Type = clause.Type + Type = clause.Type, + Alias = clause.Alias, }; return new Query() .AddComponent("aggregate", outerClause) - .From(query, $"{clause.Type}Query"); + // Use alias + capitalized type + 'query' as alias + .From(query, $"{clause.Alias}{clause.Type.First().ToString().ToUpperInvariant()}{clause.Type.Substring(1)}Query"); } protected virtual SqlResult CompileRaw(Query query) @@ -472,6 +475,17 @@ public virtual string CompileColumn(SqlResult ctx, AbstractColumn column) return "(" + subCtx.RawSql + $"){alias}"; } + if (column is AggregateColumn aggregate) + { + return $"{aggregate.Type.ToUpperInvariant()}({CompileColumn(ctx, new Column { Name = aggregate.Column })}) {ColumnAsKeyword}{WrapValue(aggregate.Alias ?? aggregate.Type)}"; + } + + if (!string.IsNullOrWhiteSpace(column.Alias)) + { + return $"{Wrap((column as Column).Name)} {ColumnAsKeyword}{Wrap(column.Alias)}"; + + } + return Wrap((column as Column).Name); } @@ -528,9 +542,12 @@ protected virtual string CompileColumns(SqlResult ctx) sql = "DISTINCT " + sql; } - return "SELECT " + aggregate.Type.ToUpperInvariant() + "(" + sql + $") {ColumnAsKeyword}" + WrapValue(aggregate.Type); + return $"SELECT {aggregate.Type.ToUpperInvariant()}({sql}) {ColumnAsKeyword}{WrapValue(aggregate.Alias ?? aggregate.Type)}"; } + // Counts of multiple columns are implemented by a sub-query + // which selects 1 from every non-null record. E.g. + // SELECT COUNT(*) FROM (SELECT 1 FROM [A] WHERE [ColumnA] IS NOT NULL AND [ColumnB] IS NOT NULL) return "SELECT 1"; } diff --git a/QueryBuilder/Query.Aggregate.cs b/QueryBuilder/Query.Aggregate.cs index d4fc5057..30023ca3 100644 --- a/QueryBuilder/Query.Aggregate.cs +++ b/QueryBuilder/Query.Aggregate.cs @@ -5,55 +5,106 @@ namespace SqlKata { public partial class Query { - public Query AsAggregate(string type, string[] columns = null) + /********************************************************************** + ** Generic aggregate ** + **********************************************************************/ + public Query SelectAggregate(string type, IEnumerable columns, string alias = null) { + if (columns.Count() == 0) + { + throw new System.ArgumentException("Cannot aggregate without columns"); + } - Method = "aggregate"; + // According to ISO/IEC 9075:2016 all aggregates take only a single + // value expression argument (i.e. one column). However, for the + // special case of count(...), SqlKata implements a transform to + // a sub query. + if (columns.Count() > 1 && type != "count") + { + throw new System.ArgumentException("Cannot aggregate more than one column at once"); + } - this.ClearComponent("aggregate") - .AddComponent("aggregate", new AggregateClause + if (type != "count" || (columns.Count() == 1 && !this.IsDistinct)) { - Type = type, - Columns = columns?.ToList() ?? new List(), - }); + Method = "select"; + this.AddComponent("select", new AggregateColumn + { + Alias = alias, + Type = type, + Column = columns.First(), + }); + } + else + { + if (this.HasComponent("aggregate")) + { + throw new System.InvalidOperationException("Cannot add more than one top-level aggregate clause"); + } + Method = "aggregate"; + this.AddComponent("aggregate", new AggregateClause + { + Alias = alias, + Type = type, + Columns = columns.ToList(), + }); + } return this; } - public Query AsCount(string[] columns = null) - { - var cols = columns?.ToList() ?? new List { }; - if (!cols.Any()) - { - cols.Add("*"); - } + /********************************************************************** + ** Count ** + **********************************************************************/ + public Query SelectCount(string column = null, string alias = null) + { + return SelectCount(column != null ? new[] { column } : new string[] { }, alias); + } - return AsAggregate("count", cols.ToArray()); + public Query SelectCount(IEnumerable columns, string alias = null) + { + return SelectAggregate("count", columns.Count() == 0 ? new[] { "*" } : columns, alias); } - public Query AsAvg(string column) + + /********************************************************************** + ** Average ** + **********************************************************************/ + public Query SelectAvg(string column, string alias = null) { - return AsAggregate("avg", new string[] { column }); + return SelectAggregate("avg", new[] { column }, alias); } - public Query AsAverage(string column) + + public Query SelectAverage(string column, string alias = null) { - return AsAvg(column); + return SelectAvg(column, alias); } - public Query AsSum(string column) + + /********************************************************************** + ** Sum ** + **********************************************************************/ + public Query SelectSum(string column, string alias = null) { - return AsAggregate("sum", new[] { column }); + return SelectAggregate("sum", new[] { column }, alias); } - public Query AsMax(string column) + + /********************************************************************** + ** Maximum ** + **********************************************************************/ + public Query SelectMax(string column, string alias = null) { - return AsAggregate("max", new[] { column }); + return SelectAggregate("max", new[] { column }, alias); } - public Query AsMin(string column) + + /********************************************************************** + ** Minimum ** + **********************************************************************/ + public Query SelectMin(string column, string alias = null) { - return AsAggregate("min", new[] { column }); + return SelectAggregate("min", new[] { column }, alias); } } } diff --git a/QueryBuilder/Query.Select.cs b/QueryBuilder/Query.Select.cs index f753a388..fe6eec16 100644 --- a/QueryBuilder/Query.Select.cs +++ b/QueryBuilder/Query.Select.cs @@ -6,27 +6,35 @@ namespace SqlKata { public partial class Query { + public Query Select(params string[] columns) => + Select(columns.AsEnumerable()); - public Query Select(params string[] columns) - { - return Select(columns.AsEnumerable()); - } + public Query Select(IEnumerable columns) => + SelectAs( + columns + .Select(x => (x, null as string)) + .ToArray() + ); - public Query Select(IEnumerable columns) + /// + /// Select columns with an alias + /// + /// + public Query SelectAs(params (string, string)[] columns) { Method = "select"; columns = columns - .Select(x => Helper.ExpandExpression(x)) + .Select(x => Helper.ExpandExpression(x.Item1).Select(y => (y, x.Item2))) .SelectMany(x => x) .ToArray(); - foreach (var column in columns) { AddComponent("select", new Column { - Name = column + Name = column.Item1, + Alias = column.Item2 }); } diff --git a/SqlKata.Execution/Query.Extensions.cs b/SqlKata.Execution/Query.Extensions.cs index 60a54ff4..9d371d5a 100644 --- a/SqlKata.Execution/Query.Extensions.cs +++ b/SqlKata.Execution/Query.Extensions.cs @@ -275,71 +275,71 @@ public static async Task DeleteAsync(this Query query, IDbTransaction trans return await CreateQueryFactory(query).ExecuteAsync(query.AsDelete(), transaction, timeout, cancellationToken); } - public static T Aggregate(this Query query, string aggregateOperation, string[] columns, IDbTransaction transaction = null, int? timeout = null) + public static T SelectAggregate(this Query query, string aggregateOperation, string[] columns, IDbTransaction transaction = null, int? timeout = null) { var db = CreateQueryFactory(query); - return db.ExecuteScalar(query.AsAggregate(aggregateOperation, columns), transaction, timeout); + return db.ExecuteScalar(query.SelectAggregate(aggregateOperation, columns), transaction, timeout); } - public static async Task AggregateAsync(this Query query, string aggregateOperation, string[] columns, IDbTransaction transaction = null, int? timeout = null, CancellationToken cancellationToken = default) + public static async Task SelectAggregateAsync(this Query query, string aggregateOperation, string[] columns, IDbTransaction transaction = null, int? timeout = null, CancellationToken cancellationToken = default) { var db = CreateQueryFactory(query); - return await db.ExecuteScalarAsync(query.AsAggregate(aggregateOperation, columns), transaction, timeout, cancellationToken); + return await db.ExecuteScalarAsync(query.SelectAggregate(aggregateOperation, columns), transaction, timeout, cancellationToken); } - public static T Count(this Query query, string[] columns = null, IDbTransaction transaction = null, int? timeout = null) + public static T SelectCount(this Query query, string[] columns = null, IDbTransaction transaction = null, int? timeout = null) { var db = CreateQueryFactory(query); - return db.ExecuteScalar(query.AsCount(columns), transaction, timeout); + return db.ExecuteScalar(query.SelectCount(columns), transaction, timeout); } - public static async Task CountAsync(this Query query, string[] columns = null, IDbTransaction transaction = null, int? timeout = null, CancellationToken cancellationToken = default) + public static async Task SelectCountAsync(this Query query, string[] columns = null, IDbTransaction transaction = null, int? timeout = null, CancellationToken cancellationToken = default) { var db = CreateQueryFactory(query); - return await db.ExecuteScalarAsync(query.AsCount(columns), transaction, timeout, cancellationToken); + return await db.ExecuteScalarAsync(query.SelectCount(columns), transaction, timeout, cancellationToken); } - public static T Average(this Query query, string column, IDbTransaction transaction = null, int? timeout = null) + public static T SelectAverage(this Query query, string column, IDbTransaction transaction = null, int? timeout = null) { - return query.Aggregate("avg", new[] { column }, transaction, timeout); + return query.SelectAggregate("avg", new[] { column }, transaction, timeout); } - public static async Task AverageAsync(this Query query, string column, IDbTransaction transaction = null, int? timeout = null, CancellationToken cancellationToken = default) + public static async Task SelectAverageAsync(this Query query, string column, IDbTransaction transaction = null, int? timeout = null, CancellationToken cancellationToken = default) { - return await query.AggregateAsync("avg", new[] { column }, transaction, timeout, cancellationToken); + return await query.SelectAggregateAsync("avg", new[] { column }, transaction, timeout, cancellationToken); } - public static T Sum(this Query query, string column, IDbTransaction transaction = null, int? timeout = null) + public static T SelectSum(this Query query, string column, IDbTransaction transaction = null, int? timeout = null) { - return query.Aggregate("sum", new[] { column }, transaction, timeout); + return query.SelectAggregate("sum", new[] { column }, transaction, timeout); } - public static async Task SumAsync(this Query query, string column, IDbTransaction transaction = null, int? timeout = null, CancellationToken cancellationToken = default) + public static async Task SelectSumAsync(this Query query, string column, IDbTransaction transaction = null, int? timeout = null, CancellationToken cancellationToken = default) { - return await query.AggregateAsync("sum", new[] { column }, transaction, timeout, cancellationToken); + return await query.SelectAggregateAsync("sum", new[] { column }, transaction, timeout, cancellationToken); } - public static T Min(this Query query, string column, IDbTransaction transaction = null, int? timeout = null) + public static T SelectMin(this Query query, string column, IDbTransaction transaction = null, int? timeout = null) { - return query.Aggregate("min", new[] { column }, transaction, timeout); + return query.SelectAggregate("min", new[] { column }, transaction, timeout); } - public static async Task MinAsync(this Query query, string column, IDbTransaction transaction = null, int? timeout = null, CancellationToken cancellationToken = default) + public static async Task SelectMinAsync(this Query query, string column, IDbTransaction transaction = null, int? timeout = null, CancellationToken cancellationToken = default) { - return await query.AggregateAsync("min", new[] { column }, transaction, timeout, cancellationToken); + return await query.SelectAggregateAsync("min", new[] { column }, transaction, timeout, cancellationToken); } - public static T Max(this Query query, string column, IDbTransaction transaction = null, int? timeout = null) + public static T SelectMax(this Query query, string column, IDbTransaction transaction = null, int? timeout = null) { - return query.Aggregate("max", new[] { column }, transaction, timeout); + return query.SelectAggregate("max", new[] { column }, transaction, timeout); } - public static async Task MaxAsync(this Query query, string column, IDbTransaction transaction = null, int? timeout = null, CancellationToken cancellationToken = default) + public static async Task SelectMaxAsync(this Query query, string column, IDbTransaction transaction = null, int? timeout = null, CancellationToken cancellationToken = default) { - return await query.AggregateAsync("max", new[] { column }, transaction, timeout, cancellationToken); + return await query.SelectAggregateAsync("max", new[] { column }, transaction, timeout, cancellationToken); } internal static XQuery CastToXQuery(Query query, string method = null) diff --git a/SqlKata.Execution/QueryFactory.cs b/SqlKata.Execution/QueryFactory.cs index b10677ab..d050fae7 100644 --- a/SqlKata.Execution/QueryFactory.cs +++ b/SqlKata.Execution/QueryFactory.cs @@ -285,7 +285,7 @@ public SqlMapper.GridReader GetMultiple( public async Task GetMultipleAsync( Query[] queries, IDbTransaction transaction = null, - int? timeout = null, + int? timeout = null, CancellationToken cancellationToken = default) { var compiled = this.Compiler.Compile(queries); @@ -324,7 +324,7 @@ public IEnumerable> Get( public async Task>> GetAsync( Query[] queries, IDbTransaction transaction = null, - int? timeout = null, + int? timeout = null, CancellationToken cancellationToken = default ) { @@ -372,7 +372,7 @@ public async Task ExistsAsync(Query query, IDbTransaction transaction = nu return rows.Any(); } - public T Aggregate( + public T SelectAggregate( Query query, string aggregateOperation, string[] columns = null, @@ -380,78 +380,78 @@ public T Aggregate( int? timeout = null ) { - return this.ExecuteScalar(query.AsAggregate(aggregateOperation, columns), transaction, timeout ?? this.QueryTimeout); + return this.ExecuteScalar(query.SelectAggregate(aggregateOperation, columns), transaction, timeout ?? this.QueryTimeout); } - public async Task AggregateAsync( + public async Task SelectAggregateAsync( Query query, string aggregateOperation, string[] columns = null, IDbTransaction transaction = null, - int? timeout = null, + int? timeout = null, CancellationToken cancellationToken = default ) { return await this.ExecuteScalarAsync( - query.AsAggregate(aggregateOperation, columns), + query.SelectAggregate(aggregateOperation, columns), transaction, timeout, cancellationToken ); } - public T Count(Query query, string[] columns = null, IDbTransaction transaction = null, int? timeout = null) + public T SelectCount(Query query, string[] columns = null, IDbTransaction transaction = null, int? timeout = null) { return this.ExecuteScalar( - query.AsCount(columns), + query.SelectCount(columns), transaction, timeout ); } - public async Task CountAsync(Query query, string[] columns = null, IDbTransaction transaction = null, int? timeout = null, CancellationToken cancellationToken = default) + public async Task SelectCountAsync(Query query, string[] columns = null, IDbTransaction transaction = null, int? timeout = null, CancellationToken cancellationToken = default) { - return await this.ExecuteScalarAsync(query.AsCount(columns), transaction, timeout, cancellationToken); + return await this.ExecuteScalarAsync(query.SelectCount(columns), transaction, timeout, cancellationToken); } - public T Average(Query query, string column, IDbTransaction transaction = null, int? timeout = null) + public T SelectAverage(Query query, string column, IDbTransaction transaction = null, int? timeout = null) { - return this.Aggregate(query, "avg", new[] { column }); + return this.SelectAggregate(query, "avg", new[] { column }); } - public async Task AverageAsync(Query query, string column, CancellationToken cancellationToken = default) + public async Task SelectAverageAsync(Query query, string column, CancellationToken cancellationToken = default) { - return await this.AggregateAsync(query, "avg", new[] { column }, cancellationToken: cancellationToken); + return await this.SelectAggregateAsync(query, "avg", new[] { column }, cancellationToken: cancellationToken); } - public T Sum(Query query, string column) + public T SelectSum(Query query, string column) { - return this.Aggregate(query, "sum", new[] { column }); + return this.SelectAggregate(query, "sum", new[] { column }); } - public async Task SumAsync(Query query, string column, CancellationToken cancellationToken = default) + public async Task SelectSumAsync(Query query, string column, CancellationToken cancellationToken = default) { - return await this.AggregateAsync(query, "sum", new[] { column }, cancellationToken: cancellationToken); + return await this.SelectAggregateAsync(query, "sum", new[] { column }, cancellationToken: cancellationToken); } - public T Min(Query query, string column) + public T SelectMin(Query query, string column) { - return this.Aggregate(query, "min", new[] { column }); + return this.SelectAggregate(query, "min", new[] { column }); } - public async Task MinAsync(Query query, string column, CancellationToken cancellationToken = default) + public async Task SelectMinAsync(Query query, string column, CancellationToken cancellationToken = default) { - return await this.AggregateAsync(query, "min", new[] { column }, cancellationToken: cancellationToken); + return await this.SelectAggregateAsync(query, "min", new[] { column }, cancellationToken: cancellationToken); } - public T Max(Query query, string column) + public T SelectMax(Query query, string column) { - return this.Aggregate(query, "max", new[] { column }); + return this.SelectAggregate(query, "max", new[] { column }); } - public async Task MaxAsync(Query query, string column, CancellationToken cancellationToken = default) + public async Task SelectMaxAsync(Query query, string column, CancellationToken cancellationToken = default) { - return await this.AggregateAsync(query, "max", new[] { column }, cancellationToken: cancellationToken); + return await this.SelectAggregateAsync(query, "max", new[] { column }, cancellationToken: cancellationToken); } public PaginationResult Paginate(Query query, int page, int perPage = 25, IDbTransaction transaction = null, int? timeout = null) @@ -466,7 +466,7 @@ public PaginationResult Paginate(Query query, int page, int perPage = 25, throw new ArgumentException("PerPage param should be greater than or equal to 1", nameof(perPage)); } - var count = Count(query.Clone(), null, transaction, timeout); + var count = SelectCount(query.Clone(), null, transaction, timeout); IEnumerable list; @@ -501,7 +501,7 @@ public async Task> PaginateAsync(Query query, int page, i throw new ArgumentException("PerPage param should be greater than or equal to 1", nameof(perPage)); } - var count = await CountAsync(query.Clone(), null, transaction, timeout, cancellationToken); + var count = await SelectCountAsync(query.Clone(), null, transaction, timeout, cancellationToken); IEnumerable list; @@ -553,7 +553,7 @@ public async Task ChunkAsync( int chunkSize, Func, int, bool> func, IDbTransaction transaction = null, - int? timeout = null, + int? timeout = null, CancellationToken cancellationToken = default ) { @@ -592,7 +592,7 @@ public async Task ChunkAsync( int chunkSize, Action, int> action, IDbTransaction transaction = null, - int? timeout = null, + int? timeout = null, CancellationToken cancellationToken = default ) {