diff --git a/QueryBuilder.Tests/AggregateTests.cs b/QueryBuilder.Tests/AggregateTests.cs index 68a69842..5414547c 100644 --- a/QueryBuilder.Tests/AggregateTests.cs +++ b/QueryBuilder.Tests/AggregateTests.cs @@ -1,11 +1,70 @@ using SqlKata.Compilers; using SqlKata.Tests.Infrastructure; +using System; using Xunit; namespace SqlKata.Tests { public class AggregateTests : TestSupport { + [Fact] + public void AsAggregateEmpty() + { + Assert.Throws(() => new Query("A").AsAggregate("aggregate", new string[] { })); + } + + [Fact] + public void AsAggregate() + { + var query = new Query("A").AsAggregate("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 AsAggregateAlias() + { + var query = new Query("A").AsAggregate("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 AsAggregateMultipleColumns() + { + var query = new Query("A").AsAggregate("aggregate", new[] { "Column1", "Column2" }); + + var c = Compile(query); + + Assert.Equal("SELECT AGGREGATE(*) AS [aggregate] FROM (SELECT 1 FROM [A] WHERE [Column1] IS NOT NULL AND [Column2] IS NOT NULL) AS [AggregateQuery]", c[EngineCodes.SqlServer]); + Assert.Equal("SELECT AGGREGATE(*) AS `aggregate` FROM (SELECT 1 FROM `A` WHERE `Column1` IS NOT NULL AND `Column2` IS NOT NULL) AS `AggregateQuery`", c[EngineCodes.MySql]); + Assert.Equal("SELECT AGGREGATE(*) AS \"AGGREGATE\" FROM (SELECT 1 FROM \"A\" WHERE \"COLUMN1\" IS NOT NULL AND \"COLUMN2\" IS NOT NULL) AS \"AGGREGATEQUERY\"", c[EngineCodes.Firebird]); + Assert.Equal("SELECT AGGREGATE(*) AS \"aggregate\" FROM (SELECT 1 FROM \"A\" WHERE \"Column1\" IS NOT NULL AND \"Column2\" IS NOT NULL) AS \"AggregateQuery\"", c[EngineCodes.PostgreSql]); + } + + [Fact] + public void AsAggregateMultipleColumnsAlias() + { + var query = new Query("A").AsAggregate("aggregate", new[] { "Column1", "Column2" }, "Alias"); + + var c = Compile(query); + + Assert.Equal("SELECT AGGREGATE(*) AS [Alias] FROM (SELECT 1 FROM [A] WHERE [Column1] IS NOT NULL AND [Column2] IS NOT NULL) AS [AliasAggregateQuery]", c[EngineCodes.SqlServer]); + Assert.Equal("SELECT AGGREGATE(*) AS `Alias` FROM (SELECT 1 FROM `A` WHERE `Column1` IS NOT NULL AND `Column2` IS NOT NULL) AS `AliasAggregateQuery`", c[EngineCodes.MySql]); + Assert.Equal("SELECT AGGREGATE(*) AS \"ALIAS\" FROM (SELECT 1 FROM \"A\" WHERE \"COLUMN1\" IS NOT NULL AND \"COLUMN2\" IS NOT NULL) AS \"ALIASAGGREGATEQUERY\"", c[EngineCodes.Firebird]); + Assert.Equal("SELECT AGGREGATE(*) AS \"Alias\" FROM (SELECT 1 FROM \"A\" WHERE \"Column1\" IS NOT NULL AND \"Column2\" IS NOT NULL) AS \"AliasAggregateQuery\"", c[EngineCodes.PostgreSql]); + } + [Fact] public void Count() { @@ -19,6 +78,49 @@ public void Count() Assert.Equal("SELECT COUNT(*) AS \"COUNT\" FROM \"A\"", c[EngineCodes.Firebird]); } + [Fact] + public void CountAsStarAlias() + { + var query = new Query("A").AsCountAs("*", "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 CountAsColumnAlias() + { + var query = new Query("A").AsCountAs("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 CountDoesntModifyColumns() + { + { + var columns = new string[] { }; + var query = new Query("A").AsCount(columns); + Compile(query); + Assert.Equal(columns, new string[] { }); + } + { + var columns = new[] { "ColumnA", "ColumnB" }; + var query = new Query("A").AsCount(columns); + Compile(query); + Assert.Equal(columns, new[] { "ColumnA", "ColumnB" }); + } + } + [Fact] public void CountMultipleColumns() { @@ -26,7 +128,17 @@ public void CountMultipleColumns() 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 [count] FROM (SELECT 1 FROM [A] WHERE [ColumnA] IS NOT NULL AND [ColumnB] IS NOT NULL) AS [CountQuery]", c[EngineCodes.SqlServer]); + } + + [Fact] + public void CountAsMultipleColumns() + { + var query = new Query("A").AsCountAs(new[] { "ColumnA", "ColumnB" }, "Alias"); + + var c = Compile(query); + + 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] @@ -36,7 +148,7 @@ public void DistinctCount() 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] @@ -46,7 +158,7 @@ public void DistinctCountMultipleColumns() 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] @@ -59,6 +171,16 @@ public void Average() Assert.Equal("SELECT AVG([TTL]) AS [avg] FROM [A]", c[EngineCodes.SqlServer]); } + [Fact] + public void AverageAlias() + { + var query = new Query("A").AsAverageAs("TTL", "Alias"); + + var c = Compile(query); + + Assert.Equal("SELECT AVG([TTL]) AS [Alias] FROM [A]", c[EngineCodes.SqlServer]); + } + [Fact] public void Sum() { @@ -69,6 +191,16 @@ public void Sum() Assert.Equal("SELECT SUM([PacketsDropped]) AS [sum] FROM [A]", c[EngineCodes.SqlServer]); } + [Fact] + public void SumAlias() + { + var query = new Query("A").AsSumAs("PacketsDropped", "Alias"); + + var c = Compile(query); + + Assert.Equal("SELECT SUM([PacketsDropped]) AS [Alias] FROM [A]", c[EngineCodes.SqlServer]); + } + [Fact] public void Max() { @@ -79,6 +211,16 @@ public void Max() Assert.Equal("SELECT MAX([LatencyMs]) AS [max] FROM [A]", c[EngineCodes.SqlServer]); } + [Fact] + public void MaxAlias() + { + var query = new Query("A").AsMaxAs("LatencyMs", "Alias"); + + var c = Compile(query); + + Assert.Equal("SELECT MAX([LatencyMs]) AS [Alias] FROM [A]", c[EngineCodes.SqlServer]); + } + [Fact] public void Min() { @@ -88,5 +230,15 @@ public void Min() Assert.Equal("SELECT MIN([LatencyMs]) AS [min] FROM [A]", c[EngineCodes.SqlServer]); } + + [Fact] + public void MinAlias() + { + var query = new Query("A").AsMinAs("LatencyMs", "Alias"); + + var c = Compile(query); + + Assert.Equal("SELECT MIN([LatencyMs]) AS [Alias] FROM [A]", c[EngineCodes.SqlServer]); + } } } diff --git a/QueryBuilder.Tests/SelectTests.cs b/QueryBuilder.Tests/SelectTests.cs index 8341bf1f..cd6270be 100644 --- a/QueryBuilder.Tests/SelectTests.cs +++ b/QueryBuilder.Tests/SelectTests.cs @@ -35,6 +35,45 @@ public void BasicSelectEnumerable() Assert.Equal("SELECT \"id\", \"name\" FROM \"users\"", c[EngineCodes.Oracle]); } + [Fact] + public void SelectAsOneColumn() + { + 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 SelectAsSingletonList() + { + 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 +113,21 @@ 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, as opposed to regular Select() when + // combining the expanded syntax and the 'as' SQLKata keyword support + // which simply silently stops working when the {...} expansion is + // applied. + 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() { 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..9de7282d 100644 --- a/QueryBuilder/Clauses/ColumnClause.cs +++ b/QueryBuilder/Clauses/ColumnClause.cs @@ -1,7 +1,10 @@ +using System.Diagnostics; + namespace SqlKata { public abstract class AbstractColumn : AbstractClause { + public string Alias { get; set; } } /// @@ -26,6 +29,7 @@ public override AbstractClause Clone() Engine = Engine, Name = Name, Component = Component, + Alias = Alias, }; } } @@ -50,6 +54,7 @@ public override AbstractClause Clone() Engine = Engine, Query = Query.Clone(), Component = Component, + Alias = Alias, }; } } @@ -68,6 +73,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..34ca59d5 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,12 @@ public virtual string CompileColumn(SqlResult ctx, AbstractColumn column) return "(" + subCtx.RawSql + $"){alias}"; } + if (!string.IsNullOrWhiteSpace(column.Alias)) + { + return $"{Wrap((column as Column).Name)} {ColumnAsKeyword}{Wrap(column.Alias)}"; + + } + return Wrap((column as Column).Name); } @@ -528,9 +537,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..6d881102 100644 --- a/QueryBuilder/Query.Aggregate.cs +++ b/QueryBuilder/Query.Aggregate.cs @@ -5,8 +5,15 @@ namespace SqlKata { public partial class Query { - public Query AsAggregate(string type, string[] columns = null) + /********************************************************************** + ** Generic aggregate ** + **********************************************************************/ + public Query AsAggregate(string type, IEnumerable columns, string alias = null) { + if (columns.Count() == 0) + { + throw new System.ArgumentException("Cannot aggregate without columns"); + } Method = "aggregate"; @@ -14,15 +21,19 @@ public Query AsAggregate(string type, string[] columns = null) .AddComponent("aggregate", new AggregateClause { Type = type, - Columns = columns?.ToList() ?? new List(), + Columns = columns.ToList(), + Alias = alias }); return this; } - public Query AsCount(string[] columns = null) + /********************************************************************** + ** Count ** + **********************************************************************/ + public Query AsCount(params string[] columns) { - var cols = columns?.ToList() ?? new List { }; + var cols = columns.ToList(); if (!cols.Any()) { @@ -32,28 +43,56 @@ public Query AsCount(string[] columns = null) return AsAggregate("count", cols.ToArray()); } - public Query AsAvg(string column) - { - return AsAggregate("avg", new string[] { column }); - } - public Query AsAverage(string column) - { - return AsAvg(column); - } + public Query AsCountAs(string column, string alias) => + AsAggregate("count", new string[] { column }, alias); - public Query AsSum(string column) - { - return AsAggregate("sum", new[] { column }); - } + public Query AsCountAs(IEnumerable columns, string alias) => + AsAggregate("count", columns, alias); - public Query AsMax(string column) - { - return AsAggregate("max", new[] { column }); - } - public Query AsMin(string column) - { - return AsAggregate("min", new[] { column }); - } + /********************************************************************** + ** Average ** + **********************************************************************/ + public Query AsAvg(string column) => + AsAverage(column); + + public Query AsAverage(string column) => + AsAggregate("avg", new[] { column }, null); + + public Query AsAvgAs(string column, string alias) => + AsAverageAs(column, alias); + + public Query AsAverageAs(string column, string alias) => + AsAggregate("avg", new[] { column }, alias); + + + /********************************************************************** + ** Sum ** + **********************************************************************/ + public Query AsSum(string column) => + AsAggregate("sum", new[] { column }, null); + + public Query AsSumAs(string column, string alias = null) => + AsAggregate("sum", new[] { column }, alias); + + + /********************************************************************** + ** Maximum ** + **********************************************************************/ + public Query AsMax(string column) => + AsAggregate("max", new[] { column }, null); + + public Query AsMaxAs(string column, string alias = null) => + AsAggregate("max", new[] { column }, alias); + + + /********************************************************************** + ** Minimum ** + **********************************************************************/ + public Query AsMin(string column) => + AsAggregate("min", new[] { column }, null); + + public Query AsMinAs(string column, string alias = null) => + AsAggregate("min", new[] { column }, alias); } } diff --git a/QueryBuilder/Query.Select.cs b/QueryBuilder/Query.Select.cs index f753a388..1d9ca7be 100644 --- a/QueryBuilder/Query.Select.cs +++ b/QueryBuilder/Query.Select.cs @@ -6,27 +6,42 @@ 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() + ); + + /// + /// Select a column with an alias + /// + /// + public Query SelectAs(string column, string alias) => + SelectAs((column, alias)); - 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 }); }