Skip to content

Commit

Permalink
Queryable null and Empty Strings (#506)
Browse files Browse the repository at this point in the history
* adding queryable null and empty strings feature
  • Loading branch information
slorello89 authored Nov 26, 2024
1 parent 7783fa3 commit 3ab039b
Show file tree
Hide file tree
Showing 15 changed files with 454 additions and 127 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ public IEnumerable<string> Serialize()
/// <inheritdoc/>
protected override void ValidateAndPushOperand(Expression expression, Stack<string> stack)
{
var dialect = 1;
if (expression is BinaryExpression binaryExpression)
{
var memberExpression = binaryExpression.Left as MemberExpression;
Expand Down Expand Up @@ -81,7 +82,7 @@ protected override void ValidateAndPushOperand(Expression expression, Stack<stri
}
else
{
var val = ExpressionParserUtilities.GetOperandStringForQueryArgs(binaryExpression.Right, new List<object>()); // hack - will need to revisit when integrating vectors into aggregations.
var val = ExpressionParserUtilities.GetOperandStringForQueryArgs(binaryExpression.Right, new List<object>(), ref dialect); // hack - will need to revisit when integrating vectors into aggregations.
stack.Push(BuildQueryPredicate(binaryExpression.NodeType, memberExpression, val));
}
}
Expand All @@ -92,7 +93,7 @@ protected override void ValidateAndPushOperand(Expression expression, Stack<stri
}
else if (expression is MethodCallExpression method)
{
stack.Push(ExpressionParserUtilities.TranslateMethodExpressions(method, new List<object>()));
stack.Push(ExpressionParserUtilities.TranslateMethodExpressions(method, new List<object>(), ref dialect));
}
else if (expression is UnaryExpression uni)
{
Expand Down
78 changes: 62 additions & 16 deletions src/Redis.OM/Common/ExpressionParserUtilities.cs
Original file line number Diff line number Diff line change
Expand Up @@ -76,19 +76,20 @@ internal static string GetOperandString(MethodCallExpression exp)
/// </summary>
/// <param name="exp">expression.</param>
/// <param name="parameters">The parameters.</param>
/// <param name="dialectNeeded">The required dialect by the final query expression.</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, bool treatBooleanMemberAsUnary = false)
internal static string GetOperandStringForQueryArgs(Expression exp, List<object> parameters, ref int dialectNeeded, bool treatEnumsAsInt = false, bool negate = false, bool treatBooleanMemberAsUnary = false)
{
var res = exp switch
{
ConstantExpression constExp => ValueToString(constExp.Value),
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, treatBooleanMemberAsUnary: treatBooleanMemberAsUnary),
MethodCallExpression method => TranslateMethodStandardQuerySyntax(method, parameters, ref dialectNeeded),
UnaryExpression unary => GetOperandStringForQueryArgs(unary.Operand, parameters, ref dialectNeeded, treatEnumsAsInt, unary.NodeType == ExpressionType.Not, treatBooleanMemberAsUnary: treatBooleanMemberAsUnary),
_ => throw new ArgumentException("Unrecognized Expression type")
};

Expand All @@ -97,6 +98,12 @@ internal static string GetOperandStringForQueryArgs(Expression exp, List<object>
negate = false;
}

if (string.IsNullOrEmpty(res))
{
res = "\"\"";
dialectNeeded |= 2;
}

if (negate)
{
return $"-{res}";
Expand Down Expand Up @@ -182,13 +189,14 @@ internal static string ParseBinaryExpression(BinaryExpression rootBinaryExpressi
/// </summary>
/// <param name="exp">the expression.</param>
/// <param name="parameters">The parameters to be passed into the query.</param>
/// <param name="dialectNeeded">The dialect required by the final query expression.</param>
/// <returns>The expression translated.</returns>
/// <exception cref="ArgumentException">thrown if the method isn't recognized.</exception>
internal static string TranslateMethodExpressions(MethodCallExpression exp, List<object> parameters)
internal static string TranslateMethodExpressions(MethodCallExpression exp, List<object> parameters, ref int dialectNeeded)
{
return exp.Method.Name switch
{
"Contains" => TranslateContainsStandardQuerySyntax(exp, parameters),
"Contains" => TranslateContainsStandardQuerySyntax(exp, parameters, ref dialectNeeded),
nameof(StringExtension.FuzzyMatch) => TranslateFuzzyMatch(exp),
nameof(StringExtension.MatchContains) => TranslateMatchContains(exp),
nameof(StringExtension.MatchPattern) => TranslateMatchPattern(exp),
Expand All @@ -197,7 +205,7 @@ internal static string TranslateMethodExpressions(MethodCallExpression exp, List
nameof(VectorExtensions.VectorRange) => TranslateVectorRange(exp, parameters),
nameof(string.StartsWith) => TranslateStartsWith(exp),
nameof(string.EndsWith) => TranslateEndsWith(exp),
"Any" => TranslateAnyForEmbeddedObjects(exp, parameters),
"Any" => TranslateAnyForEmbeddedObjects(exp, parameters, ref dialectNeeded),
_ => throw new ArgumentException($"Unrecognized method for query translation:{exp.Method.Name}")
};
}
Expand Down Expand Up @@ -316,6 +324,44 @@ internal static string EscapeTagField(string text)
return sb.ToString();
}

/// <summary>
/// Checks the expression if it resolves to null.
/// </summary>
/// <param name="expression">The expression to check.</param>
/// <returns>whether it resolves to null.</returns>
internal static bool ExpressionResolvesToNull(Expression expression)
{
if (expression.NodeType is ExpressionType.Constant && ((ConstantExpression)expression).Value is null)
{
return true;
}

if (expression.NodeType is ExpressionType.MemberAccess)
{
var parentExpression = expression;
var memberInfos = new Stack<MemberInfo>();
while (parentExpression is MemberExpression parentMember)
{
var info = ((MemberExpression)parentExpression).Member;
memberInfos.Push(info);
parentExpression = parentMember.Expression;
}

if (parentExpression is ConstantExpression c)
{
var resolved = c.Value;
foreach (var info in memberInfos)
{
resolved = GetValue(info, resolved);
}

return resolved is null;
}
}

return false;
}

private static string GetOperandStringForMember(MemberExpression member, bool treatEnumsAsInt = false, bool negate = false, bool treatBooleanMemberAsUnary = false)
{
var memberPath = new List<string>();
Expand Down Expand Up @@ -659,7 +705,7 @@ private static IEnumerable<BinaryExpression> SplitBinaryExpression(BinaryExpress
while (true);
}

private static string TranslateMethodStandardQuerySyntax(MethodCallExpression exp, List<object> parameters)
private static string TranslateMethodStandardQuerySyntax(MethodCallExpression exp, List<object> parameters, ref int dialectNeeded)
{
return exp.Method.Name switch
{
Expand All @@ -669,11 +715,11 @@ private static string TranslateMethodStandardQuerySyntax(MethodCallExpression ex
nameof(StringExtension.MatchEndsWith) => TranslateEndsWith(exp),
nameof(StringExtension.MatchPattern) => TranslateMatchPattern(exp),
nameof(string.Format) => TranslateFormatMethodStandardQuerySyntax(exp),
nameof(string.Contains) => TranslateContainsStandardQuerySyntax(exp, parameters),
nameof(string.Contains) => TranslateContainsStandardQuerySyntax(exp, parameters, ref dialectNeeded),
nameof(string.StartsWith) => TranslateStartsWith(exp),
nameof(string.EndsWith) => TranslateEndsWith(exp),
nameof(VectorExtensions.VectorRange) => TranslateVectorRange(exp, parameters),
"Any" => TranslateAnyForEmbeddedObjects(exp, parameters),
"Any" => TranslateAnyForEmbeddedObjects(exp, parameters, ref dialectNeeded),
_ => throw new InvalidOperationException($"Unable to parse method {exp.Method.Name}")
};
}
Expand Down Expand Up @@ -817,7 +863,7 @@ private static string TranslateFuzzyMatch(MethodCallExpression exp)
};
}

private static string TranslateContainsStandardQuerySyntax(MethodCallExpression exp, List<object> parameters)
private static string TranslateContainsStandardQuerySyntax(MethodCallExpression exp, List<object> parameters, ref int dialectNeeded)
{
MemberExpression? expression = null;
Type type;
Expand All @@ -828,7 +874,7 @@ private static string TranslateContainsStandardQuerySyntax(MethodCallExpression
{
var propertyExpression = (MemberExpression)exp.Arguments.Last();
var valuesExpression = (MemberExpression)exp.Arguments.First();
literal = GetOperandStringForQueryArgs(propertyExpression, parameters);
literal = GetOperandStringForQueryArgs(propertyExpression, parameters, ref dialectNeeded);
if (!literal.StartsWith("@"))
{
if (exp.Arguments.Count == 1 && exp.Object != null)
Expand Down Expand Up @@ -867,7 +913,7 @@ private static string TranslateContainsStandardQuerySyntax(MethodCallExpression
var valueType = Nullable.GetUnderlyingType(valuesExpression.Type) ?? valuesExpression.Type;
memberName = GetOperandStringForMember(propertyExpression);
var treatEnumsAsInts = type.IsEnum && !(propertyExpression.Member.GetCustomAttributes(typeof(JsonConverterAttribute)).FirstOrDefault() is JsonConverterAttribute converter && converter.ConverterType == typeof(JsonStringEnumConverter));
literal = GetOperandStringForQueryArgs(valuesExpression, parameters, treatEnumsAsInts);
literal = GetOperandStringForQueryArgs(valuesExpression, parameters, ref dialectNeeded, treatEnumsAsInts);

if ((valueType == typeof(List<string>) || valueType == typeof(string[]) || type == typeof(string[]) || type == typeof(List<string>) || type == typeof(Guid) || type == typeof(Guid[]) || type == typeof(List<Guid>) || type == typeof(Guid[]) || type == typeof(List<Guid>) || type == typeof(Ulid) || (type.IsEnum && !treatEnumsAsInts)) && attribute is IndexedAttribute)
{
Expand Down Expand Up @@ -918,7 +964,7 @@ private static string TranslateContainsStandardQuerySyntax(MethodCallExpression

type = Nullable.GetUnderlyingType(expression.Type) ?? expression.Type;
memberName = GetOperandStringForMember(expression);
literal = GetOperandStringForQueryArgs(exp.Arguments.Last(), parameters);
literal = GetOperandStringForQueryArgs(exp.Arguments.Last(), parameters, ref dialectNeeded);

if (searchFieldAttribute is not null && searchFieldAttribute is SearchableAttribute)
{
Expand Down Expand Up @@ -967,20 +1013,20 @@ private static string GetContainsStringForConstantExpression(string propertyName
return sb.ToString();
}

private static string TranslateAnyForEmbeddedObjects(MethodCallExpression exp, List<object> parameters)
private static string TranslateAnyForEmbeddedObjects(MethodCallExpression exp, List<object> parameters, ref int dialectNeeded)
{
var type = exp.Arguments.Last().Type;
var prefix = GetOperandString(exp.Arguments[0]);
var lambda = (LambdaExpression)exp.Arguments.Last();

if (lambda.Body is MethodCallExpression methodCall)
{
var tempQuery = TranslateMethodExpressions(methodCall, parameters);
var tempQuery = TranslateMethodExpressions(methodCall, parameters, ref dialectNeeded);
return tempQuery.Replace("@", $"{prefix}_");
}
else
{
var tempQuery = ExpressionTranslator.TranslateBinaryExpression((BinaryExpression)lambda.Body, parameters);
var tempQuery = ExpressionTranslator.TranslateBinaryExpression((BinaryExpression)lambda.Body, parameters, ref dialectNeeded);
return tempQuery.Replace("@", $"{prefix}_");
}
}
Expand Down
Loading

0 comments on commit 3ab039b

Please sign in to comment.