Skip to content

Commit

Permalink
Merge pull request #151 from OctopusDeploy/andrew-w/group-by-trial
Browse files Browse the repository at this point in the history
Adding GroupBy Support
  • Loading branch information
andrew-at-octopus authored Jun 23, 2021
2 parents 9c791d7 + b524c52 commit fc926f8
Show file tree
Hide file tree
Showing 8 changed files with 179 additions and 24 deletions.
48 changes: 48 additions & 0 deletions source/Nevermore.Tests/QueryBuilderFixture/QueryBuilderFixture.cs
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,7 @@ public void ShouldGenerateSelectForComplicatedSubqueryJoin()
.Where("IsActive = 1")
.OrderBy("Created");

// Note, this is less than ideal, Hint should be multi-add, and perhaps with an enum??
var accounts = CreateQueryBuilder<object>("Accounts").Hint("WITH (UPDLOCK)");

var actual = orders.InnerJoin(customers.Subquery())
Expand Down Expand Up @@ -257,6 +258,53 @@ FROM [dbo].[Orders] NOLOCK
actual.Should().Be(expected);
}

[Test]
public void ShouldGenerateGroupBySpaceForQueryBuilder()
{
string actual = null;
transaction.Stream<object>(Arg.Do<string>(s => actual = s), Arg.Any<CommandParameterValues>());

CreateQueryBuilder<object>("Orders")
.NoLock()
.GroupBy("SpaceId")
.Column("SpaceId")
.CalculatedColumn("COUNT (*)", "OrderCount")
.ToList();

const string expected = @"SELECT [SpaceId],
COUNT (*) AS [OrderCount]
FROM [dbo].[Orders] NOLOCK
GROUP BY [SpaceId]";

actual.Should().Be(expected);
}


[Test]
public void ShouldGenerateGroupBySpaceWithAliasForQueryBuilder()
{
string actual = null;
transaction.Stream<object>(Arg.Do<string>(s => actual = s), Arg.Any<CommandParameterValues>());

CreateQueryBuilder<object>("Orders", "scheme")
.Alias("Agg")
.NoLock()
.GroupBy("SpaceId", "Agg")
.Column("SpaceId", "SpaceId", "Agg")
.CalculatedColumn("COUNT (*)", "OrderCount")
.CalculatedColumn("MAX(Agg.[DataVersion])", "Latest")
.ToList();

// NOTE, this is invalid SQL, will be fixed in separate PR
const string expected = @"SELECT Agg.[SpaceId] AS [SpaceId],
COUNT (*) AS [OrderCount],
MAX(Agg.[DataVersion]) AS [Latest]
FROM [scheme].[Orders] Agg NOLOCK
GROUP BY Agg.[SpaceId]";

actual.Should().Be(expected);
}

[Test]
public void ShouldGenerateCountForJoin()
{
Expand Down
14 changes: 13 additions & 1 deletion source/Nevermore/Advanced/QueryBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ public IQueryBuilder<TRecord> CalculatedColumn(string expression, string columnA
selectBuilder.AddColumnSelection(new AliasedColumn(new CalculatedColumn(new CustomExpression(expression)), columnAlias));
return this;
}

public IQueryBuilder<TNewRecord> AsType<TNewRecord>() where TNewRecord : class
{
return new QueryBuilder<TNewRecord, TSelectBuilder>(selectBuilder, readQueryExecutor, tableAliasGenerator, uniqueParameterNameGenerator, ParameterValues, Parameters, ParameterDefaults);
Expand Down Expand Up @@ -199,6 +199,18 @@ public ISelectBuilder GetSelectBuilder()
return selectBuilder.Clone();
}

public IQueryBuilder<TRecord> GroupBy(string fieldName)
{
selectBuilder.AddGroupBy(fieldName);
return this;
}

public IQueryBuilder<TRecord> GroupBy(string fieldName, string tableAlias)
{
selectBuilder.AddGroupBy(fieldName, tableAlias);
return this;
}

public IOrderedQueryBuilder<TRecord> OrderBy(string fieldName)
{
selectBuilder.AddOrder(fieldName, false);
Expand Down
59 changes: 40 additions & 19 deletions source/Nevermore/Advanced/SelectBuilder.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using Nevermore.Querying;
using Nevermore.Querying.AST;

Expand All @@ -10,13 +11,13 @@ public class JoinSelectBuilder : SelectBuilderBase<JoinedSource>
{
protected override JoinedSource From { get; }

public JoinSelectBuilder(JoinedSource from) : this(from, new List<IWhereClause>(), new List<OrderByField>())
public JoinSelectBuilder(JoinedSource from) : this(from, new List<IWhereClause>(), new List<GroupByField>(), new List<OrderByField>())
{
}

JoinSelectBuilder(JoinedSource from, List<IWhereClause> whereClauses, List<OrderByField> orderByClauses,
JoinSelectBuilder(JoinedSource from, List<IWhereClause> whereClauses, List<GroupByField> groupByClauses, List<OrderByField> orderByClauses,
ISelectColumns columnSelection = null, IRowSelection rowSelection = null)
: base(whereClauses, orderByClauses, columnSelection, rowSelection)
: base(whereClauses, groupByClauses, orderByClauses, columnSelection, rowSelection)
{
From = from;
}
Expand All @@ -30,7 +31,7 @@ protected override IEnumerable<OrderByField> GetDefaultOrderByFields()

public override ISelectBuilder Clone()
{
return new JoinSelectBuilder(From, new List<IWhereClause>(WhereClauses), new List<OrderByField>(OrderByClauses), ColumnSelection, RowSelection);
return new JoinSelectBuilder(From, new List<IWhereClause>(WhereClauses), new List<GroupByField>(GroupByClauses), new List<OrderByField>(OrderByClauses), ColumnSelection, RowSelection);
}

public override void AddWhere(UnaryWhereParameter whereParams)
Expand Down Expand Up @@ -75,14 +76,14 @@ public override void AddRowNumberColumn(string alias, IReadOnlyList<Column> part
public class TableSelectBuilder : SelectBuilderBase<ITableSource>
{
public TableSelectBuilder(ITableSource from, IColumn idColumn)
: this(from, idColumn, new List<IWhereClause>(), new List<OrderByField>())
: this(from, idColumn, new List<IWhereClause>(), new List<GroupByField>(), new List<OrderByField>())
{
}

TableSelectBuilder(ITableSource from, IColumn idColumn, List<IWhereClause> whereClauses,
TableSelectBuilder(ITableSource from, IColumn idColumn, List<IWhereClause> whereClauses, List<GroupByField> groupByClauses,
List<OrderByField> orderByClauses, ISelectColumns columnSelection = null,
IRowSelection rowSelection = null)
: base(whereClauses, orderByClauses, columnSelection, rowSelection)
: base(whereClauses, groupByClauses, orderByClauses, columnSelection, rowSelection)
{
From = from;
IdColumn = idColumn;
Expand All @@ -99,7 +100,7 @@ protected override IEnumerable<OrderByField> GetDefaultOrderByFields()

public override ISelectBuilder Clone()
{
return new TableSelectBuilder(From, IdColumn, new List<IWhereClause>(WhereClauses), new List<OrderByField>(OrderByClauses), ColumnSelection, RowSelection);
return new TableSelectBuilder(From, IdColumn, new List<IWhereClause>(WhereClauses), new List<GroupByField>(GroupByClauses), new List<OrderByField>(OrderByClauses), ColumnSelection, RowSelection);
}
}

Expand All @@ -112,13 +113,13 @@ public class UnionSelectBuilder : SelectBuilderBase<ISubquerySource>
public UnionSelectBuilder(ISelect innerSelect,
string customAlias,
ITableAliasGenerator tableAliasGenerator)
: this(innerSelect, customAlias, tableAliasGenerator, new List<IWhereClause>(), new List<OrderByField>())
: this(innerSelect, customAlias, tableAliasGenerator, new List<IWhereClause>(), new List<GroupByField>(), new List<OrderByField>())
{
}

UnionSelectBuilder(ISelect innerSelect, string customAlias, ITableAliasGenerator tableAliasGenerator, List<IWhereClause> whereClauses, List<OrderByField> orderByClauses,
UnionSelectBuilder(ISelect innerSelect, string customAlias, ITableAliasGenerator tableAliasGenerator, List<IWhereClause> whereClauses, List<GroupByField> groupByClauses, List<OrderByField> orderByClauses,
ISelectColumns columnSelection = null, IRowSelection rowSelection = null)
: base(whereClauses, orderByClauses, columnSelection, rowSelection)
: base(whereClauses, groupByClauses, orderByClauses, columnSelection, rowSelection)
{
this.innerSelect = innerSelect;
this.customAlias = customAlias;
Expand Down Expand Up @@ -154,20 +155,20 @@ protected override IEnumerable<OrderByField> GetDefaultOrderByFields()

public override ISelectBuilder Clone()
{
return new UnionSelectBuilder(innerSelect, customAlias, tableAliasGenerator, new List<IWhereClause>(WhereClauses), new List<OrderByField>(OrderByClauses), ColumnSelection, RowSelection);
return new UnionSelectBuilder(innerSelect, customAlias, tableAliasGenerator, new List<IWhereClause>(WhereClauses), new List<GroupByField>(GroupByClauses), new List<OrderByField>(OrderByClauses), ColumnSelection, RowSelection);
}
}

public class SubquerySelectBuilder : SelectBuilderBase<ISubquerySource>
{
public SubquerySelectBuilder(ISubquerySource from)
: this(from, new List<IWhereClause>(), new List<OrderByField>())
: this(from, new List<IWhereClause>(), new List<GroupByField>(), new List<OrderByField>())
{
}

SubquerySelectBuilder(ISubquerySource from, List<IWhereClause> whereClauses, List<OrderByField> orderByClauses,
SubquerySelectBuilder(ISubquerySource from, List<IWhereClause> whereClauses, List<GroupByField> groupByClauses, List<OrderByField> orderByClauses,
ISelectColumns columnSelection = null, IRowSelection rowSelection = null)
: base(whereClauses, orderByClauses, columnSelection, rowSelection)
: base(whereClauses, groupByClauses, orderByClauses, columnSelection, rowSelection)
{
From = @from;
}
Expand All @@ -182,24 +183,29 @@ protected override IEnumerable<OrderByField> GetDefaultOrderByFields()

public override ISelectBuilder Clone()
{
return new SubquerySelectBuilder(From, new List<IWhereClause>(WhereClauses), new List<OrderByField>(OrderByClauses), ColumnSelection, RowSelection);
return new SubquerySelectBuilder(From, new List<IWhereClause>(WhereClauses), new List<GroupByField>(GroupByClauses), new List<OrderByField>(OrderByClauses), ColumnSelection, RowSelection);
}
}

public abstract class SelectBuilderBase<TSource> : ISelectBuilder where TSource : ISelectSource
{
protected abstract TSource From { get; }
protected readonly List<OrderByField> OrderByClauses;
protected readonly List<GroupByField> GroupByClauses;
protected readonly List<IWhereClause> WhereClauses;
protected ISelectColumns ColumnSelection;
protected IRowSelection RowSelection;

protected SelectBuilderBase(List<IWhereClause> whereClauses, List<OrderByField> orderByClauses,
protected SelectBuilderBase(
List<IWhereClause> whereClauses,
List<GroupByField> groupByClauses,
List<OrderByField> orderByClauses,
ISelectColumns columnSelection = null,
IRowSelection rowSelection = null)
{
WhereClauses = whereClauses;
OrderByClauses = orderByClauses;
GroupByClauses = groupByClauses;
this.RowSelection = rowSelection;
this.ColumnSelection = columnSelection;
}
Expand Down Expand Up @@ -227,7 +233,7 @@ public virtual ISelect GenerateSelectWithoutDefaultOrderBy()

ISelect GenerateSelectInner(Func<OrderBy> getDefaultOrderBy)
{
return new Select(GetRowSelection(), GetColumnSelection(), From, GetWhere() ?? new Where(), GetOrderBy(getDefaultOrderBy));
return new Select(GetRowSelection(), GetColumnSelection(), From, GetWhere() ?? new Where(), GetGroupBy(), GetOrderBy(getDefaultOrderBy));
}

public abstract ISelectBuilder Clone();
Expand All @@ -237,10 +243,15 @@ Where GetWhere()
return WhereClauses.Any() ? new Where(new AndClause(WhereClauses)) : null;
}

GroupBy GetGroupBy()
{
return GroupByClauses.Any() ? new GroupBy(GroupByClauses) : null;
}

OrderBy GetOrderBy(Func<OrderBy> getDefaultOrderBy)
{
// If you are doing something like COUNT(*) then it doesn't make sense to include an Order By clause
if (GetColumnSelection().AggregatesRows)
if (GetColumnSelection().AggregatesRows || GroupByClauses.Any())
{
return null;
}
Expand All @@ -266,6 +277,16 @@ public void AddDistinct()
RowSelection = new Distinct();
}

public void AddGroupBy(string fieldName)
{
GroupByClauses.Add(new GroupByField(new Column(fieldName)));
}

public void AddGroupBy(string fieldName, string tableAlias)
{
GroupByClauses.Add(new GroupByField(new TableColumn(new Column(fieldName), tableAlias)));
}

public virtual void AddOrder(string fieldName, bool @descending)
{
OrderByClauses.Add(new OrderByField(new Column(fieldName), @descending ? OrderByDirection.Descending : OrderByDirection.Ascending));
Expand Down
12 changes: 11 additions & 1 deletion source/Nevermore/Advanced/SourceQueryBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -350,6 +350,16 @@ public IArrayParametersQueryBuilder<TRecord> WhereParameterized(string fieldName
return Final(Builder.WhereParameterized(fieldName, operand, parameterNames));
}

public IQueryBuilder<TRecord> GroupBy(string fieldName)
{
return Final(Builder.GroupBy(fieldName));
}

public IQueryBuilder<TRecord> GroupBy(string fieldName, string tableAlias)
{
return Final(Builder.GroupBy(fieldName, tableAlias));
}

public IOrderedQueryBuilder<TRecord> OrderBy(string fieldName)
{
return Final(Builder.OrderBy(fieldName));
Expand Down Expand Up @@ -394,7 +404,7 @@ public IQueryBuilder<TRecord> CalculatedColumn(string expression, string columnA
{
return Builder.CalculatedColumn(expression, columnAlias);
}

public IQueryBuilder<TNewRecord> AsType<TNewRecord>() where TNewRecord : class
{
return Builder.AsType<TNewRecord>();
Expand Down
17 changes: 16 additions & 1 deletion source/Nevermore/IQueryBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,21 @@ public interface IQueryBuilder<TRecord> : ICompleteQuery<TRecord> where TRecord
IQueryBuilder<TRecord> WhereNull(string fieldName);
IQueryBuilder<TRecord> WhereNotNull(string fieldName);

/// <summary>
/// Adds a group by clause to the query. If this is used, then all selected should be included in the group by, or be a calculated column
/// </summary>
/// <param name="fieldName">The column that the query should be grouped by</param>
/// <returns>The query builder that can be used to further modify the query, or execute the query</returns>
IQueryBuilder<TRecord> GroupBy(string fieldName);

/// <summary>
/// Adds a group by clause to the query. If this is used, then all selected should be included in the group by, or be a calculated column
/// </summary>
/// <param name="fieldName">The column that the query should be grouped by</param>
/// <param name="tableAlias">The alias for where the column exists</param>
/// <returns>The query builder that can be used to further modify the query, or execute the query</returns>
IQueryBuilder<TRecord> GroupBy(string fieldName, string tableAlias);

/// <summary>
/// Adds an order by clause to the query, where the order by clause will be in the default order (ascending).
/// If no order by clauses are added to the query, the query will be ordered by the Id column in ascending order.
Expand Down Expand Up @@ -211,7 +226,7 @@ public interface IQueryBuilder<TRecord> : ICompleteQuery<TRecord> where TRecord
/// <param name="columnAlias">The alias for the calculated column</param>
/// <returns>The query builder that can be used to further modify the query, or execute the query</returns>
IQueryBuilder<TRecord> CalculatedColumn(string expression, string columnAlias);

/// <summary>
/// Change the type of the record returned by the QueryBuilder.
/// This is useful if the initial type no longer matches the columns returned by the query.
Expand Down
5 changes: 5 additions & 0 deletions source/Nevermore/ISelectBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,12 @@ namespace Nevermore
public interface ISelectBuilder
{
void AddTop(int top);

void AddDistinct();

void AddGroupBy(string fieldName);
void AddGroupBy(string fieldName, string tableAlias);

void AddOrder(string fieldName, bool descending);
void AddOrder(string fieldName, string tableAlias, bool descending);
void AddWhere(UnaryWhereParameter whereParams);
Expand Down
39 changes: 39 additions & 0 deletions source/Nevermore/Querying/AST/GroupBy.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
using System;
using System.Collections.Generic;
using System.Linq;

namespace Nevermore.Querying.AST
{
public class GroupBy
{
readonly IReadOnlyList<GroupByField> fields;

public GroupBy(IReadOnlyList<GroupByField> fields)
{
if (fields.Count < 1) throw new ArgumentException("Fields must have at least one value");
this.fields = fields;
}

public string GenerateSql()
{
return @$"
GROUP BY {string.Join(@", ", fields.Select(f => f.GenerateSql()))}";
}

public override string ToString() => GenerateSql();
}

public class GroupByField
{
readonly IColumn column;

public GroupByField(IColumn column)
{
this.column = column;
}

public string GenerateSql() => column.GenerateSql();

public override string ToString() => GenerateSql();
}
}
Loading

0 comments on commit fc926f8

Please sign in to comment.