Skip to content

Commit

Permalink
HavingClause first test pass
Browse files Browse the repository at this point in the history
  • Loading branch information
markrendle committed Nov 14, 2011
1 parent 8e6d19f commit 6412991
Show file tree
Hide file tree
Showing 9 changed files with 334 additions and 18 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,5 @@ apply_config.bat
NDependOut
*.dotCover
*_mm_cache.bin
*.idc
Simple.Data.sln.DotSettings.user
9 changes: 9 additions & 0 deletions Simple.Data.InMemoryTest/InMemoryTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,15 @@ public void SelectWithMaxShouldReturnMax()
Assert.AreEqual(50, records[1].MaxAge);
}

[Test]
public void SelectWithHavingSumShouldReturnOnlyMatchingRows()
{
var db = CreateAggregateTestDb();
var records = db.Test.All().Select(db.Test.Name).Having(db.Test.Age.Sum() > 50).ToList();
Assert.AreEqual(1, records.Count);
Assert.AreEqual("Bob", records[0].Name);
}

private static dynamic CreateAggregateTestDb()
{
Database.UseMockAdapter(new InMemoryAdapter());
Expand Down
9 changes: 9 additions & 0 deletions Simple.Data/AllColumnsSpecialReference.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
namespace Simple.Data
{
public class AllColumnsSpecialReference : SpecialReference
{
public AllColumnsSpecialReference() : base("*")
{
}
}
}
123 changes: 106 additions & 17 deletions Simple.Data/QueryPolyfills/DictionaryQueryRunner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,14 @@

internal class DictionaryQueryRunner
{
const string AutoColumnPrefix = "___having___";
private static readonly
Dictionary<Type, Func<SimpleQueryClauseBase, IEnumerable<IDictionary<string, object>>, IEnumerable<IDictionary<string, object>>>> ClauseHandlers =
new Dictionary<Type, Func<SimpleQueryClauseBase, IEnumerable<IDictionary<string, object>>, IEnumerable<IDictionary<string, object>>>>
{
{ typeof(DistinctClause), (c,d) => d.Distinct(new DictionaryEqualityComparer()) },
{ typeof(SkipClause), (c,d) => d.Skip(((SkipClause)c).Count) },
{ typeof(TakeClause), (c,d) => d.Take(((TakeClause)c).Count) },
{ typeof(SelectClause), (c,d) => new SelectClauseHandler((SelectClause)c).Run(d) },
{ typeof(WhereClause), (c,d) => new WhereClauseHandler((WhereClause)c).Run(d) },
{ typeof(OrderByClause), (c, d) => new OrderByClauseHandler((OrderByClause)c).Run(d) }
};

Expand All @@ -37,15 +36,13 @@ public DictionaryQueryRunner(IEnumerable<IDictionary<string, object>> source, pa

public IEnumerable<IDictionary<string, object>> Run()
{
IEnumerable<IDictionary<string, object>> source;
var source = RunWhereClauses(_source);

if (_withCountClause != null)
{
source = _source.ToList();
_withCountClause.SetCount(source.Count());
}
else
{
source = _source;
var list = source.ToList();
_withCountClause.SetCount(list.Count);
source = list;
}

foreach (var clause in _clauses)
Expand All @@ -57,24 +54,116 @@ public IEnumerable<IDictionary<string, object>> Run()
}
}

source = RunHavingClauses(source);
source = RunSelectClauses(source);
return source;
}

private IEnumerable<IDictionary<string, object>> RunWhereClauses(IEnumerable<IDictionary<string, object>> source)
{
foreach (var whereClause in _clauses.OfType<WhereClause>())
{
source = new WhereClauseHandler(whereClause).Run(source);
}
return source;
}

private IEnumerable<IDictionary<string, object>> RunSelectClauses(IEnumerable<IDictionary<string, object>> source)
{
foreach (var selectClause in _clauses.OfType<SelectClause>())
{
source = new SelectClauseHandler(selectClause).Run(source);
}
return source;
}

private IEnumerable<IDictionary<string, object>> RunHavingClauses(IEnumerable<IDictionary<string, object>> source)
{
var havingClauses = _clauses.OfType<HavingClause>().ToList();
if (havingClauses.Count == 0) return source;

var selectClause = _clauses.OfType<SelectClause>().FirstOrDefault();

List<SimpleReference> selectReferences;

if (selectClause != null)
{
selectReferences = selectClause.Columns.ToList();
}
else
{
selectReferences = new List<SimpleReference> { new AllColumnsSpecialReference() };
}

foreach (var clause in havingClauses)
{
var criteria = HavingToWhere(clause.Criteria, selectReferences);
source = new SelectClauseHandler(new SelectClause(selectReferences)).Run(source).ToList();
source = new WhereClauseHandler(new WhereClause(criteria)).Run(source);
source = source.Select(d => d.Where(kvp => !kvp.Key.StartsWith(AutoColumnPrefix)).ToDictionary(kvp => kvp.Key, kvp => kvp.Value));
}

return source;
}

private SimpleExpression HavingToWhere(SimpleExpression criteria, List<SimpleReference> selectReferences)
{
if (criteria.LeftOperand is SimpleExpression)
{
return new SimpleExpression(HavingToWhere((SimpleExpression) criteria.LeftOperand, selectReferences),
HavingToWhere((SimpleExpression) criteria.RightOperand, selectReferences),
criteria.Type);
}

object leftOperand = ReplaceFunctionOperand(criteria.LeftOperand, selectReferences);
object rightOperand = ReplaceFunctionOperand(criteria.RightOperand, selectReferences);

return new SimpleExpression(leftOperand, rightOperand, criteria.Type);
}

private static object ReplaceFunctionOperand(object operand, List<SimpleReference> selectReferences)
{
var leftFunction = operand as FunctionReference;
if (!leftFunction.IsNull())
{
var alias = AutoColumnPrefix + Guid.NewGuid().ToString("N");
selectReferences.Add(leftFunction.As(alias));
return new ObjectReference(alias);
}
return operand;
}

private static bool AreEquivalentReferences(FunctionReference reference1, FunctionReference reference2)
{
if (reference1.IsNull()) return reference2.IsNull();
if (reference2.IsNull()) return false;
return reference1.Name == reference2.Name && reference1.Argument == reference2.Argument;
}
}

internal class OrderByClauseHandler
internal class GenericEqualityComparer<T> : IEqualityComparer<T>
{
private readonly OrderByClause _orderByClause;
private readonly Func<T, T, bool> _equals;
private readonly Func<T, int> _getHashCode;

public GenericEqualityComparer(Func<T, T, bool> @equals) : this(@equals, _ => 1)
{
}

public GenericEqualityComparer(Func<T, T, bool> @equals, Func<T, int> getHashCode)
{
_equals = @equals;
_getHashCode = getHashCode;
}

public OrderByClauseHandler(OrderByClause orderByClause)
public bool Equals(T x, T y)
{
_orderByClause = orderByClause;
return _equals(x, y);
}

public IEnumerable<IDictionary<string, object>> Run(IEnumerable<IDictionary<string, object>> source)
public int GetHashCode(T obj)
{
return _orderByClause.Direction == OrderByDirection.Ascending
? source.OrderBy(d => d[_orderByClause.Reference.GetName()])
: source.OrderByDescending(d => d[_orderByClause.Reference.GetName()]);
return _getHashCode(obj);
}
}
}
162 changes: 162 additions & 0 deletions Simple.Data/QueryPolyfills/HavingClauseHandler.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;

namespace Simple.Data.QueryPolyfills
{
internal class HavingClauseHandler
{
private readonly Dictionary<SimpleExpressionType, Func<SimpleExpression, Func<IDictionary<string, object>, bool>>> _expressionFormatters;

private readonly WhereClause _whereClause;

public HavingClauseHandler(WhereClause whereClause)
{
_whereClause = whereClause;
_expressionFormatters = new Dictionary<SimpleExpressionType, Func<SimpleExpression, Func<IDictionary<string,object>, bool>>>
{
{SimpleExpressionType.And, LogicalExpressionToWhereClause},
{SimpleExpressionType.Or, LogicalExpressionToWhereClause},
{SimpleExpressionType.Equal, EqualExpressionToWhereClause},
{SimpleExpressionType.NotEqual, NotEqualExpressionToWhereClause},
{SimpleExpressionType.Function, FunctionExpressionToWhereClause},
{SimpleExpressionType.GreaterThan, GreaterThanToWhereClause},
{SimpleExpressionType.LessThan, LessThanToWhereClause},
{SimpleExpressionType.GreaterThanOrEqual, GreaterThanOrEqualToWhereClause},
{SimpleExpressionType.LessThanOrEqual, LessThanOrEqualToWhereClause},
{SimpleExpressionType.Empty, expr => _ => true },
};
}

private Func<IDictionary<string, object>, bool> FunctionExpressionToWhereClause(SimpleExpression arg)
{
var key = GetKeyFromLeftOperand(arg);
var function = arg.RightOperand as SimpleFunction;
if (ReferenceEquals(function, null)) throw new InvalidOperationException("Expression type of function but no function supplied.");
if (function.Name.Equals("like", StringComparison.OrdinalIgnoreCase))
{
var pattern = function.Args[0].ToString();
if (pattern.Contains("%") || pattern.Contains("_")) // SQL Server LIKE
{
pattern = pattern.Replace("%", ".*").Replace('_', '.');
}

var regex = new Regex("^" + pattern + "$", RegexOptions.Multiline | RegexOptions.IgnoreCase);

return d => d.ContainsKey(key) && (!ReferenceEquals(d[key], null)) && regex.IsMatch(d[key].ToString());
}

throw new NotSupportedException("Expression Function not supported.");
}

private Func<IDictionary<string, object>, bool> GreaterThanToWhereClause(SimpleExpression arg)
{
var key = GetKeyFromLeftOperand(arg);

return d => d.ContainsKey(key) && !ReferenceEquals(d[key], null) && ((IComparable)d[key]).CompareTo(arg.RightOperand) > 0;
}

private Func<IDictionary<string, object>, bool> LessThanToWhereClause(SimpleExpression arg)
{
var key = GetKeyFromLeftOperand(arg);

return d => d.ContainsKey(key) && !ReferenceEquals(d[key], null) && ((IComparable)d[key]).CompareTo(arg.RightOperand) < 0;
}

private Func<IDictionary<string, object>, bool> GreaterThanOrEqualToWhereClause(SimpleExpression arg)
{
var key = GetKeyFromLeftOperand(arg);

return d => d.ContainsKey(key) && !ReferenceEquals(d[key], null) && ((IComparable)d[key]).CompareTo(arg.RightOperand) >= 0;
}

private Func<IDictionary<string, object>, bool> LessThanOrEqualToWhereClause(SimpleExpression arg)
{
var key = GetKeyFromLeftOperand(arg);

return d => d.ContainsKey(key) && !ReferenceEquals(d[key], null) && ((IComparable)d[key]).CompareTo(arg.RightOperand) <= 0;
}

private Func<IDictionary<string, object>, bool> NotEqualExpressionToWhereClause(SimpleExpression arg)
{
var key = GetKeyFromLeftOperand(arg);

if (ReferenceEquals(arg.RightOperand, null))
{
return d => d.ContainsKey(key) && d[key] != null;
}

if (arg.RightOperand.GetType().IsArray)
{
return
d =>
d.ContainsKey(key) &&
!((IEnumerable)d[key]).Cast<object>().SequenceEqual(((IEnumerable)arg.RightOperand).Cast<object>());
}

return d => d.ContainsKey(key) && !Equals(d[key], arg.RightOperand);
}

private Func<IDictionary<string, object>, bool> EqualExpressionToWhereClause(SimpleExpression arg)
{
var key = GetKeyFromLeftOperand(arg);

if (ReferenceEquals(arg.RightOperand, null))
{
return d => (!d.ContainsKey(key)) || d[key] == null;
}

if (arg.RightOperand.GetType().IsArray)
{
return
d =>
d.ContainsKey(key) &&
((IEnumerable) d[key]).Cast<object>().SequenceEqual(((IEnumerable) arg.RightOperand).Cast<object>());
}

return d => d.ContainsKey(key) && Equals(d[key], arg.RightOperand);
}

private static string GetKeyFromLeftOperand(SimpleExpression arg)
{
var reference = arg.LeftOperand as ObjectReference;

if (reference.IsNull()) throw new NotSupportedException("Only ObjectReference types are supported.");

var key = reference.GetName();
return key;
}

private Func<IDictionary<string,object>, bool> Format(SimpleExpression expression)
{
Func<SimpleExpression, Func<IDictionary<string,object>,bool>> formatter;

if (_expressionFormatters.TryGetValue(expression.Type, out formatter))
{
return formatter(expression);
}

return _ => true;
}

private Func<IDictionary<string, object>, bool> LogicalExpressionToWhereClause(SimpleExpression arg)
{
var left = Format((SimpleExpression) arg.LeftOperand);
var right = Format((SimpleExpression) arg.RightOperand);

if (arg.Type == SimpleExpressionType.Or)
{
return d => (left(d) || right(d));
}
return d => (left(d) && right(d));
}

public IEnumerable<IDictionary<string, object>> Run(IEnumerable<IDictionary<string, object>> source)
{
var predicate = Format(_whereClause.Criteria);
return source.Where(predicate);
}
}
}
22 changes: 22 additions & 0 deletions Simple.Data/QueryPolyfills/OrderByClauseHandler.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
using System.Collections.Generic;
using System.Linq;

namespace Simple.Data.QueryPolyfills
{
internal class OrderByClauseHandler
{
private readonly OrderByClause _orderByClause;

public OrderByClauseHandler(OrderByClause orderByClause)
{
_orderByClause = orderByClause;
}

public IEnumerable<IDictionary<string, object>> Run(IEnumerable<IDictionary<string, object>> source)
{
return _orderByClause.Direction == OrderByDirection.Ascending
? source.OrderBy(d => d[_orderByClause.Reference.GetName()])
: source.OrderByDescending(d => d[_orderByClause.Reference.GetName()]);
}
}
}
2 changes: 1 addition & 1 deletion Simple.Data/QueryPolyfills/SelectClauseHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ class SelectClauseHandler
private readonly IList<SimpleReference> _references;
private readonly IList<ValueResolver> _resolvers;
private Func<int, IDictionary<string, object>> _creator;
private GroupingHandler _groupingHandler;
private readonly GroupingHandler _groupingHandler;

public SelectClauseHandler(SelectClause clause)
{
Expand Down
Loading

0 comments on commit 6412991

Please sign in to comment.