Skip to content

Commit

Permalink
fixing multi-bool logic parsing logic
Browse files Browse the repository at this point in the history
  • Loading branch information
slorello89 committed Dec 18, 2023
1 parent b199bfc commit 5ea3c0e
Show file tree
Hide file tree
Showing 3 changed files with 117 additions and 10 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
73 changes: 71 additions & 2 deletions test/Redis.OM.Unit.Tests/RediSearchTests/SearchTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2877,17 +2877,86 @@ 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.Where(x => x.Boolean).Count();
var count = collection.Count(x => x.Boolean && x.AnEnum == AnEnum.three && x.Boolean == false);
_substitute.Received().Execute(
"FT.SEARCH",
"objectwithstringlikevaluetypes-idx",
"@Boolean:{true}",
"((@Boolean:{true} (@AnEnum:{three})) (@Boolean:{False}))",
"LIMIT",
"0",
"0");
Expand Down

0 comments on commit 5ea3c0e

Please sign in to comment.