Skip to content

Commit

Permalink
Adding arg-less count to RedisCollection (#426)
Browse files Browse the repository at this point in the history
* argless count to RedisCollection

* fixing multi-bool logic parsing logic
  • Loading branch information
slorello89 committed Apr 12, 2024
1 parent 9412a3f commit 37d573f
Show file tree
Hide file tree
Showing 5 changed files with 146 additions and 8 deletions.
20 changes: 16 additions & 4 deletions src/Redis.OM/Common/ExpressionParserUtilities.cs
Original file line number Diff line number Diff line change
Expand Up @@ -77,19 +77,25 @@ internal static string GetOperandString(MethodCallExpression exp)
/// <param name="parameters">The parameters.</param>
/// <param name="treatEnumsAsInt">Treat enum as an integer.</param>
/// <param name="negate">Whether or not to negate the result.</param>
/// <param name="treatBooleanMemberAsUnary">Treats a boolean member expression as unary.</param>
/// <returns>the operand string.</returns>
/// <exception cref="ArgumentException">thrown if expression is un-parseable.</exception>
internal static string GetOperandStringForQueryArgs(Expression exp, List<object> parameters, bool treatEnumsAsInt = false, bool negate = false)
internal static string GetOperandStringForQueryArgs(Expression exp, List<object> parameters, bool treatEnumsAsInt = false, bool negate = false, bool treatBooleanMemberAsUnary = false)
{
var res = exp switch
{
ConstantExpression constExp => ValueToString(constExp.Value),
MemberExpression member => GetOperandStringForMember(member, treatEnumsAsInt),
MemberExpression member => GetOperandStringForMember(member, treatEnumsAsInt, negate: negate, treatBooleanMemberAsUnary: treatBooleanMemberAsUnary),
MethodCallExpression method => TranslateMethodStandardQuerySyntax(method, parameters),
UnaryExpression unary => GetOperandStringForQueryArgs(unary.Operand, parameters, treatEnumsAsInt, unary.NodeType == ExpressionType.Not),
UnaryExpression unary => GetOperandStringForQueryArgs(unary.Operand, parameters, treatEnumsAsInt, unary.NodeType == ExpressionType.Not, treatBooleanMemberAsUnary: treatBooleanMemberAsUnary),
_ => throw new ArgumentException("Unrecognized Expression type")
};

if (treatBooleanMemberAsUnary && exp is MemberExpression memberExp && memberExp.Type == typeof(bool) && negate)
{
negate = false;
}

if (negate)
{
return $"-{res}";
Expand Down Expand Up @@ -308,7 +314,7 @@ internal static string EscapeTagField(string text)
return sb.ToString();
}

private static string GetOperandStringForMember(MemberExpression member, bool treatEnumsAsInt = false)
private static string GetOperandStringForMember(MemberExpression member, bool treatEnumsAsInt = false, bool negate = false, bool treatBooleanMemberAsUnary = false)
{
var memberPath = new List<string>();
var parentExpression = member.Expression;
Expand Down Expand Up @@ -414,6 +420,12 @@ private static string GetOperandStringForMember(MemberExpression member, bool tr
if (searchField != null)
{
var propertyName = GetSearchFieldNameFromMember(member);
if (member.Type == typeof(bool) && treatBooleanMemberAsUnary)
{
var val = negate ? "false" : "true";
return $"@{propertyName}:{{{val}}}";
}

return $"@{propertyName}";
}

Expand Down
34 changes: 30 additions & 4 deletions src/Redis.OM/Common/ExpressionTranslator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -349,22 +349,22 @@ internal static string TranslateBinaryExpression(BinaryExpression binExpression,
sb.Append("(");
sb.Append(TranslateBinaryExpression(left, parameters));
sb.Append(SplitPredicateSeporators(binExpression.NodeType));
sb.Append(ExpressionParserUtilities.GetOperandStringForQueryArgs(binExpression.Right, parameters));
sb.Append(ExpressionParserUtilities.GetOperandStringForQueryArgs(binExpression.Right, parameters, treatBooleanMemberAsUnary: true));
sb.Append(")");
}
else if (binExpression.Right is BinaryExpression right)
{
sb.Append("(");
sb.Append(ExpressionParserUtilities.GetOperandStringForQueryArgs(binExpression.Left, parameters));
sb.Append(ExpressionParserUtilities.GetOperandStringForQueryArgs(binExpression.Left, parameters, treatBooleanMemberAsUnary: true));
sb.Append(SplitPredicateSeporators(binExpression.NodeType));
sb.Append(TranslateBinaryExpression(right, parameters));
sb.Append(")");
}
else
{
var leftContent = ExpressionParserUtilities.GetOperandStringForQueryArgs(binExpression.Left, parameters);
var leftContent = ExpressionParserUtilities.GetOperandStringForQueryArgs(binExpression.Left, parameters, treatBooleanMemberAsUnary: true);

var rightContent = ExpressionParserUtilities.GetOperandStringForQueryArgs(binExpression.Right, parameters);
var rightContent = ExpressionParserUtilities.GetOperandStringForQueryArgs(binExpression.Right, parameters, treatBooleanMemberAsUnary: true);

if (binExpression.Left is MemberExpression member)
{
Expand Down Expand Up @@ -737,6 +737,24 @@ private static RedisSortBy TranslateOrderByMethod(MethodCallExpression expressio
return sb;
}

private static string TranslateUnaryOrMemberExpressionIntoBooleanQuery(Expression expression, List<object> parameters)
{
if (expression is MemberExpression member && member.Type == typeof(bool))
{
var propertyName = ExpressionParserUtilities.GetOperandStringForQueryArgs(member, parameters);
return $"{propertyName}:{{true}}";
}

if (expression is UnaryExpression uni && uni.Operand is MemberExpression uniMember && uniMember.Type == typeof(bool) && uni.NodeType is ExpressionType.Not)
{
var propertyName = ExpressionParserUtilities.GetOperandStringForQueryArgs(uniMember, parameters);
return $"{propertyName}:{{false}}";
}

throw new InvalidOperationException(
$"Could not translate expression of type {expression.Type} to a boolean expression");
}

private static string BuildQueryFromExpression(Expression exp, List<object> parameters)
{
if (exp is BinaryExpression binExp)
Expand All @@ -751,6 +769,12 @@ private static string BuildQueryFromExpression(Expression exp, List<object> para

if (exp is UnaryExpression uni)
{
if (uni.Operand is MemberExpression uniMember && uniMember.Type == typeof(bool) && uni.NodeType is ExpressionType.Not)
{
var propertyName = ExpressionParserUtilities.GetOperandString(uniMember);
return $"{propertyName}:{{false}}";
}

var operandString = BuildQueryFromExpression(uni.Operand, parameters);
if (uni.NodeType == ExpressionType.Not)
{
Expand Down Expand Up @@ -786,6 +810,8 @@ private static string BuildQueryPredicate(ExpressionType expType, string left, s
ExpressionType.LessThanOrEqual => $"{left}:[-inf {right}]",
ExpressionType.Equal => BuildEqualityPredicate(memberExpression, right),
ExpressionType.NotEqual => BuildEqualityPredicate(memberExpression, right, true),
ExpressionType.And or ExpressionType.AndAlso => $"{left} {right}",
ExpressionType.Or or ExpressionType.OrElse => $"{left} | {right}",
_ => string.Empty
};
return queryPredicate;
Expand Down
6 changes: 6 additions & 0 deletions src/Redis.OM/Searching/IRedisCollection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,12 @@ public interface IRedisCollection<T> : IOrderedQueryable<T>, IAsyncEnumerable<T>
/// <returns>The Collection's count.</returns>
int Count(Expression<Func<T, bool>> expression);

/// <summary>
/// Retrieves the count of the collection async.
/// </summary>
/// <returns>The Collection's count.</returns>
int Count();

/// <summary>
/// Returns the first item asynchronously.
/// </summary>
Expand Down
8 changes: 8 additions & 0 deletions src/Redis.OM/Searching/RedisCollection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -459,6 +459,14 @@ public int Count(Expression<Func<T, bool>> expression)
return (int)_connection.Search<T>(query).DocumentCount;
}

/// <inheritdoc />
public int Count()
{
var query = ExpressionTranslator.BuildQueryFromExpression(Expression, typeof(T), BooleanExpression, RootType);
query.Limit = new SearchLimit { Number = 0, Offset = 0 };
return (int)_connection.Search<T>(query).DocumentCount;
}

/// <inheritdoc />
public T First(Expression<Func<T, bool>> expression)
{
Expand Down
86 changes: 86 additions & 0 deletions test/Redis.OM.Unit.Tests/RediSearchTests/SearchTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2877,6 +2877,92 @@ public void SearchTagFieldAndTextListContainsWithEscapes()
"100");
}

[Fact]
public void ChainTwoBooleans()
{
int count;
_substitute.ClearSubstitute();
_substitute.Execute(Arg.Any<string>(), Arg.Any<object[]>()).Returns(_mockReply);
IRedisCollection<ObjectWithStringLikeValueTypes> collection = new RedisCollection<ObjectWithStringLikeValueTypes>(_substitute);
collection = collection.Where(x => x.Boolean);
collection = collection.Where((x => x.Boolean));
count = collection.Count();
_substitute.Received().Execute("FT.SEARCH", "objectwithstringlikevaluetypes-idx", "(@Boolean:{true} @Boolean:{true})", "LIMIT", "0", "0");
Assert.Equal(1, count);

collection = new RedisCollection<ObjectWithStringLikeValueTypes>(_substitute);
collection = collection.Where(x => x.Boolean || x.Boolean);
count = collection.Count();
_substitute.Received().Execute("FT.SEARCH", "objectwithstringlikevaluetypes-idx", "(@Boolean:{true} | @Boolean:{true})", "LIMIT", "0", "0");
Assert.Equal(1, count);

collection = new RedisCollection<ObjectWithStringLikeValueTypes>(_substitute);
collection = collection.Where(x => !x.Boolean || x.Boolean);
count = collection.Count();
_substitute.Received().Execute("FT.SEARCH", "objectwithstringlikevaluetypes-idx", "(@Boolean:{false} | @Boolean:{true})", "LIMIT", "0", "0");
Assert.Equal(1, count);

collection = new RedisCollection<ObjectWithStringLikeValueTypes>(_substitute);
collection = collection.Where(x => !x.Boolean);
collection = collection.Where((x => !x.Boolean));
count = collection.Count();
_substitute.Received().Execute("FT.SEARCH", "objectwithstringlikevaluetypes-idx", "(@Boolean:{false} @Boolean:{false})", "LIMIT", "0", "0");
Assert.Equal(1, count);

collection = new RedisCollection<ObjectWithStringLikeValueTypes>(_substitute);
collection = collection.Where(x => x.Boolean);
collection = collection.Where((x => !x.Boolean));
count = collection.Count();
_substitute.Received().Execute("FT.SEARCH", "objectwithstringlikevaluetypes-idx", "(@Boolean:{true} @Boolean:{false})", "LIMIT", "0", "0");
Assert.Equal(1, count);

collection = new RedisCollection<ObjectWithStringLikeValueTypes>(_substitute);
collection = collection.Where(x => !x.Boolean);
collection = collection.Where((x => x.Boolean));
count = collection.Count();
_substitute.Received().Execute("FT.SEARCH", "objectwithstringlikevaluetypes-idx", "(@Boolean:{false} @Boolean:{true})", "LIMIT", "0", "0");
Assert.Equal(1, count);

collection = new RedisCollection<ObjectWithStringLikeValueTypes>(_substitute);
collection = collection.Where(x => !x.Boolean);
collection = collection.Where((x => x.Boolean));
collection = collection.Where((x => x.Boolean));
count = collection.Count();
_substitute.Received().Execute("FT.SEARCH", "objectwithstringlikevaluetypes-idx", "((@Boolean:{false} @Boolean:{true}) @Boolean:{true})", "LIMIT", "0", "0");
Assert.Equal(1, count);

collection = new RedisCollection<ObjectWithStringLikeValueTypes>(_substitute);
collection = collection.Where(x => !x.Boolean);
collection = collection.Where((x => x.Boolean));
collection = collection.Where((x => !x.Boolean));
count = collection.Count();
_substitute.Received().Execute("FT.SEARCH", "objectwithstringlikevaluetypes-idx", "((@Boolean:{false} @Boolean:{true}) @Boolean:{false})", "LIMIT", "0", "0");
Assert.Equal(1, count);

collection = new RedisCollection<ObjectWithStringLikeValueTypes>(_substitute);
collection = collection.Where(x => !x.Boolean || x.Boolean || !x.Boolean);
count = collection.Count();
_substitute.Received().Execute("FT.SEARCH", "objectwithstringlikevaluetypes-idx", "((@Boolean:{false} | @Boolean:{true}) | @Boolean:{false})", "LIMIT", "0", "0");
Assert.Equal(1, count);
}

[Fact]
public void SearchWithEmptyCount()
{
_substitute.ClearSubstitute();
_substitute.Execute(Arg.Any<string>(), Arg.Any<object[]>()).Returns(_mockReply);
var collection = new RedisCollection<ObjectWithStringLikeValueTypes>(_substitute);
var count = collection.Count(x => x.Boolean && x.AnEnum == AnEnum.three && x.Boolean == false);
_substitute.Received().Execute(
"FT.SEARCH",
"objectwithstringlikevaluetypes-idx",
"((@Boolean:{true} (@AnEnum:{three})) (@Boolean:{False}))",
"LIMIT",
"0",
"0");
Assert.Equal(1, count);
}

[Fact]
public void SearchWithEmptyAny()
{
Expand Down

0 comments on commit 37d573f

Please sign in to comment.