Skip to content
Closed
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
12 changes: 11 additions & 1 deletion src/GoatQuery/src/Ast/Literals.cs
Original file line number Diff line number Diff line change
Expand Up @@ -95,4 +95,14 @@ public BooleanLiteral(Token token, bool value) : base(token)
{
Value = value;
}
}
}

public sealed class EnumSymbolLiteral : QueryExpression
{
public string Value { get; set; }

public EnumSymbolLiteral(Token token, string value) : base(token)
{
Value = value;
}
}
221 changes: 190 additions & 31 deletions src/GoatQuery/src/Evaluator/FilterEvaluator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -52,21 +52,27 @@ private static Result<Expression> EvaluatePropertyPathExpression(
(Expression)context.CurrentLambda.Parameter :
context.RootParameter;

var propertyPathResult = BuildPropertyPath(propertyPath, baseExpression, context.PropertyMappingTree);
if (propertyPathResult.IsFailed) return Result.Fail(propertyPathResult.Errors);
var safePathResult = BuildPropertyPathWithGuard(propertyPath.Segments, baseExpression, context.PropertyMappingTree);
if (safePathResult.IsFailed) return Result.Fail(safePathResult.Errors);

var finalProperty = propertyPathResult.Value;
var (finalProperty, guard, container) = safePathResult.Value;

if (exp.Right is NullLiteral)
{
var nullComparison = CreateNullComparison(exp, finalProperty);
return nullComparison;
return ComposeNestedNullComparison(finalProperty, guard, exp.Operator);
}

var comparisonResult = EvaluateValueComparison(exp, finalProperty);
if (comparisonResult.IsFailed) return comparisonResult;

return comparisonResult.Value;
var comparison = comparisonResult.Value;

var requireFinalNotNull = RequiresFinalNotNull(exp.Operator, finalProperty, exp.Right);
var combinedGuard = requireFinalNotNull
? Expression.AndAlso(guard, Expression.NotEqual(finalProperty, Expression.Constant(null, finalProperty.Type)))
: guard;

return Expression.AndAlso(combinedGuard, comparison);
}

private static Result<MemberExpression> BuildPropertyPath(
Expand All @@ -84,7 +90,6 @@ private static Result<MemberExpression> BuildPropertyPath(

current = Expression.Property(current, propertyNode.ActualPropertyName);

// Navigate to nested mapping for next segment
if (!isLast)
{
if (!propertyNode.HasNestedMapping)
Expand All @@ -97,6 +102,55 @@ private static Result<MemberExpression> BuildPropertyPath(
return Result.Ok((MemberExpression)current);
}

private static Result<(MemberExpression Final, Expression Guard, Expression Container)> BuildPropertyPathWithGuard(
IList<string> segments,
Expression startExpression,
PropertyMappingTree propertyMappingTree)
{
if (segments == null || segments.Count == 0)
return Result.Fail("Property path segments cannot be empty");

var current = startExpression;
var currentMappingTree = propertyMappingTree;

Expression guard = Expression.Constant(true);
Expression container = startExpression;

for (int i = 0; i < segments.Count; i++)
{
var segment = segments[i];
var isLast = i == segments.Count - 1;

if (!currentMappingTree.TryGetProperty(segment, out var propertyNode))
return Result.Fail($"Invalid property '{segment}' in path");

var next = Expression.Property(current, propertyNode.ActualPropertyName);

if (!isLast)
{
if (!propertyNode.HasNestedMapping)
return Result.Fail($"Property '{segment}' does not support nested navigation");

if (!next.Type.IsValueType || Nullable.GetUnderlyingType(next.Type) != null)
{
var notNull = Expression.NotEqual(next, Expression.Constant(null, next.Type));
guard = Expression.AndAlso(guard, notNull);
}

current = next;
container = current;
currentMappingTree = propertyNode.NestedMapping;
}
else
{
var final = Expression.Property(current, propertyNode.ActualPropertyName);
return Result.Ok(((MemberExpression)final, guard, container));
}
}

return Result.Fail("Invalid property path");
}

private static Result<MemberExpression> ResolvePropertyPathForCollection(
PropertyPath propertyPath,
Expression baseExpression,
Expand All @@ -113,8 +167,6 @@ private static Result<MemberExpression> ResolvePropertyPathForCollection(
return Result.Fail($"Invalid property '{segment}' in lambda expression property path");

current = Expression.Property(current, propertyNode.ActualPropertyName);

// Navigate to nested mapping for next segment
if (i < propertyPath.Segments.Count - 1)
{
if (!propertyNode.HasNestedMapping)
Expand Down Expand Up @@ -224,6 +276,22 @@ private static Result<Expression> CreateComparisonExpression(string operatorKeyw

private static Result<ConstantExpression> CreateConstantExpression(QueryExpression literal, Expression expression)
{
if (IsEnumOrNullableEnum(expression.Type))
{
if (literal is StringLiteral enumString)
{
return CreateEnumConstantFromString(enumString.Value, expression.Type);
}
if (literal is IntegerLiteral enumInt)
{
return CreateEnumConstantFromInteger(enumInt.Value, expression.Type);
}
if (literal is EnumSymbolLiteral enumSymbol)
{
return CreateEnumConstantFromString(enumSymbol.Value, expression.Type);
}
}

return literal switch
{
IntegerLiteral intLit => CreateIntegerConstant(intLit.Value, expression),
Expand All @@ -236,6 +304,7 @@ private static Result<ConstantExpression> CreateConstantExpression(QueryExpressi
DateTimeLiteral dtLit => Result.Ok(Expression.Constant(dtLit.Value, expression.Type)),
BooleanLiteral boolLit => Result.Ok(Expression.Constant(boolLit.Value, expression.Type)),
NullLiteral _ => Result.Ok(Expression.Constant(null, expression.Type)),
EnumSymbolLiteral enumSym => Result.Fail("Unquoted identifiers are only allowed for enum values"),
_ => Result.Fail($"Unsupported literal type: {literal.GetType().Name}")
};
}
Expand Down Expand Up @@ -326,7 +395,6 @@ private static Result<Expression> EvaluateLambdaExpression(QueryLambdaExpression

var (collectionProperty, elementType, lambdaParameter) = setupResult.Value;

// Enter lambda scope
context.EnterLambdaScope(lambdaExp.Parameter, lambdaParameter, elementType);

try
Expand Down Expand Up @@ -382,7 +450,7 @@ private static Result<MemberExpression> ResolveCollectionProperty(QueryExpressio
{
return Result.Fail($"Invalid property '{identifier.TokenLiteral()}' in lambda expression");
}
return Expression.Property(baseExpression, propertyNode.ActualPropertyName);
return Expression.Property(baseExpression, propertyNode.ActualPropertyName) as MemberExpression;

case PropertyPath propertyPath:
return ResolvePropertyPathForCollection(propertyPath, baseExpression, propertyMappingTree);
Expand All @@ -392,6 +460,42 @@ private static Result<MemberExpression> ResolveCollectionProperty(QueryExpressio
}
}

private static bool RequiresFinalNotNull(string operatorKeyword, MemberExpression finalProperty, QueryExpression right)
{
if (operatorKeyword.Equals(Keywords.Contains, StringComparison.OrdinalIgnoreCase))
return true;

if (right is NullLiteral)
return false;

var type = finalProperty.Type;
if (!type.IsValueType)
return true;

return Nullable.GetUnderlyingType(type) != null;
}

private static Expression ComposeNestedNullComparison(MemberExpression finalProperty, Expression guard, string operatorKeyword)
{
var isEq = operatorKeyword.Equals(Keywords.Eq, StringComparison.OrdinalIgnoreCase);
var isNe = operatorKeyword.Equals(Keywords.Ne, StringComparison.OrdinalIgnoreCase);
var nullConst = Expression.Constant(null, finalProperty.Type);
var finalEqNull = Expression.Equal(finalProperty, nullConst);
var finalNeNull = Expression.NotEqual(finalProperty, nullConst);
var notGuard = Expression.Not(guard);

if (isEq)
{
return Expression.OrElse(notGuard, finalEqNull);
}
else if (isNe)
{
return Expression.AndAlso(guard, finalNeNull);
}

return Expression.AndAlso(guard, finalEqNull);
}

private static Result<Expression> EvaluateLambdaBody(QueryExpression expression, FilterEvaluationContext context)
{
return expression switch
Expand Down Expand Up @@ -431,7 +535,6 @@ private static Result<Expression> EvaluateLambdaBodyIdentifier(InfixExpression e

if (identifierName.Equals(context.CurrentLambda.ParameterName, StringComparison.OrdinalIgnoreCase))
{
// For primitive types (string, int, etc.), allow direct comparisons with the lambda parameter
if (IsPrimitiveType(context.CurrentLambda.ElementType))
{
return EvaluateValueComparison(exp, context.CurrentLambda.Parameter);
Expand Down Expand Up @@ -467,28 +570,29 @@ private static Result<Expression> EvaluateLambdaBodyLogicalOperator(InfixExpress

private static Result<Expression> EvaluateLambdaPropertyPath(InfixExpression exp, PropertyPath propertyPath, ParameterExpression lambdaParameter)
{
// Skip the first segment (lambda parameter name) and build property path from lambda parameter
var current = (Expression)lambdaParameter;
var elementType = lambdaParameter.Type;
var mapping = PropertyMappingTreeBuilder.BuildMappingTree(elementType, GetDefaultMaxDepth());

// Build property path from lambda parameter
var pathResult = BuildLambdaPropertyPath(current, propertyPath.Segments.Skip(1).ToList(), elementType);
if (pathResult.IsFailed) return pathResult;
var safePathResult = BuildPropertyPathWithGuard(propertyPath.Segments.Skip(1).ToList(), lambdaParameter, mapping);
if (safePathResult.IsFailed) return Result.Fail(safePathResult.Errors);

current = pathResult.Value;
var (finalProperty, guard, container) = safePathResult.Value;

var finalProperty = (MemberExpression)current;

// Handle null comparisons
if (exp.Right is NullLiteral)
{
return exp.Operator == Keywords.Eq
? Expression.Equal(finalProperty, Expression.Constant(null, finalProperty.Type))
: Expression.NotEqual(finalProperty, Expression.Constant(null, finalProperty.Type));
return ComposeNestedNullComparison(finalProperty, guard, exp.Operator);
}

// Handle value comparisons
return EvaluateValueComparison(exp, finalProperty);
var comparisonResult = EvaluateValueComparison(exp, finalProperty);
if (comparisonResult.IsFailed) return comparisonResult;

var comparison = comparisonResult.Value;
var requireFinalNotNull = RequiresFinalNotNull(exp.Operator, finalProperty, exp.Right);
var combinedGuard = requireFinalNotNull
? Expression.AndAlso(guard, Expression.NotEqual(finalProperty, Expression.Constant(null, finalProperty.Type)))
: guard;

return Expression.AndAlso(combinedGuard, comparison);
}

private static Expression CreateAnyExpression(MemberExpression collection, LambdaExpression lambda, Type elementType)
Expand Down Expand Up @@ -522,7 +626,6 @@ private static Result<Expression> BuildLambdaPropertyPath(Expression startExpres

current = Expression.Property(current, propertyNode.ActualPropertyName);

// Update mapping tree for nested navigation
if (propertyNode.HasNestedMapping)
{
currentMappingTree = propertyNode.NestedMapping;
Expand All @@ -539,7 +642,6 @@ private static int GetDefaultMaxDepth()

private static Type GetCollectionElementType(Type collectionType)
{
// Handle IEnumerable<T>
if (collectionType.IsGenericType)
{
var genericArgs = collectionType.GetGenericArguments();
Expand All @@ -550,7 +652,6 @@ private static Type GetCollectionElementType(Type collectionType)
}
}

// Handle arrays
if (collectionType.IsArray)
{
return collectionType.GetElementType();
Expand All @@ -563,7 +664,11 @@ private static Result<ConstantExpression> GetIntegerExpressionConstant(int value
{
try
{
// Fetch the underlying type if it's nullable.
if (IsEnumOrNullableEnum(targetType))
{
return CreateEnumConstantFromInteger(value, targetType);
}

var underlyingType = Nullable.GetUnderlyingType(targetType);
var type = underlyingType ?? targetType;

Expand Down Expand Up @@ -591,4 +696,58 @@ private static Result<ConstantExpression> GetIntegerExpressionConstant(int value
return Result.Fail($"Error converting {value} to {targetType.Name}: {ex.Message}");
}
}
}

private static bool IsEnumOrNullableEnum(Type type)
{
var underlying = Nullable.GetUnderlyingType(type) ?? type;
return underlying.IsEnum;
}

private static Result<ConstantExpression> CreateEnumConstantFromString(string value, Type targetType)
{
var isNullable = Nullable.GetUnderlyingType(targetType) != null;
var enumType = Nullable.GetUnderlyingType(targetType) ?? targetType;

try
{
var enumValue = Enum.Parse(enumType, value, ignoreCase: true);

if (isNullable)
{
var nullableType = typeof(Nullable<>).MakeGenericType(enumType);
var boxedNullable = Activator.CreateInstance(nullableType, enumValue);
return Expression.Constant(boxedNullable, targetType);
}

return Expression.Constant(enumValue, targetType);
}
catch (ArgumentException)
{
return Result.Fail($"'{value}' is not a valid value for enum type {enumType.Name}");
}
}

private static Result<ConstantExpression> CreateEnumConstantFromInteger(int intValue, Type targetType)
{
var isNullable = Nullable.GetUnderlyingType(targetType) != null;
var enumType = Nullable.GetUnderlyingType(targetType) ?? targetType;

try
{
var enumValue = Enum.ToObject(enumType, intValue);

if (isNullable)
{
var nullableType = typeof(Nullable<>).MakeGenericType(enumType);
var boxedNullable = Activator.CreateInstance(nullableType, enumValue);
return Expression.Constant(boxedNullable, targetType);
}

return Expression.Constant(enumValue, targetType);
}
catch (Exception ex)
{
return Result.Fail($"Error converting integer {intValue} to enum type {enumType.Name}: {ex.Message}");
}
}
}
3 changes: 2 additions & 1 deletion src/GoatQuery/src/Parser/Parser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,7 @@ private Result<InfixExpression> ParseFilterStatement()

var statement = new InfixExpression(_currentToken, leftExpression, _currentToken.Literal);

if (!PeekTokenIn(TokenType.STRING, TokenType.INT, TokenType.GUID, TokenType.DATETIME, TokenType.DECIMAL, TokenType.FLOAT, TokenType.DOUBLE, TokenType.DATE, TokenType.NULL, TokenType.BOOLEAN))
if (!PeekTokenIn(TokenType.STRING, TokenType.INT, TokenType.GUID, TokenType.DATETIME, TokenType.DECIMAL, TokenType.FLOAT, TokenType.DOUBLE, TokenType.DATE, TokenType.NULL, TokenType.BOOLEAN, TokenType.IDENT))
{
return Result.Fail("Invalid value type within filter");
}
Expand Down Expand Up @@ -295,6 +295,7 @@ private QueryExpression ParseLiteral(Token token)
TokenType.BOOLEAN => bool.TryParse(token.Literal, out var boolValue)
? new BooleanLiteral(token, boolValue)
: null,
TokenType.IDENT => new EnumSymbolLiteral(token, token.Literal),
_ => null
};
}
Expand Down
Loading