Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Queryable null and Empty Strings #506

Merged
merged 4 commits into from
Nov 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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